298 lines
11 KiB
Python
298 lines
11 KiB
Python
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
|
import base64
|
|
import logging
|
|
|
|
from odoo import _, api, fields, models
|
|
from odoo.exceptions import UserError, ValidationError
|
|
|
|
from .. import match_algorithm
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
class FetchmailServerFolder(models.Model):
|
|
_name = "fetchmail.server.folder"
|
|
_rec_name = "path"
|
|
_order = "sequence"
|
|
|
|
def _get_match_algorithms(self):
|
|
def get_all_subclasses(cls):
|
|
return cls.__subclasses__() + [
|
|
subsub
|
|
for sub in cls.__subclasses__()
|
|
for subsub in get_all_subclasses(sub)
|
|
]
|
|
|
|
return {
|
|
cls.__name__: cls
|
|
for cls in get_all_subclasses(match_algorithm.base.Base)
|
|
}
|
|
|
|
def _get_match_algorithms_sel(self):
|
|
algorithms = []
|
|
for cls in self._get_match_algorithms().itervalues():
|
|
algorithms.append((cls.__name__, cls.name))
|
|
algorithms.sort()
|
|
return algorithms
|
|
|
|
server_id = fields.Many2one("fetchmail.server", "Server")
|
|
sequence = fields.Integer("Sequence")
|
|
state = fields.Selection(
|
|
[("draft", "Not Confirmed"), ("done", "Confirmed")],
|
|
string="Status",
|
|
readonly=True,
|
|
required=True,
|
|
copy=False,
|
|
default="draft",
|
|
)
|
|
path = fields.Char(
|
|
"Path",
|
|
required=True,
|
|
help="The path to your mail folder."
|
|
" Typically would be something like 'INBOX.myfolder'",
|
|
)
|
|
model_id = fields.Many2one(
|
|
"ir.model", "Model", required=True, 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"
|
|
"Examples:\n"
|
|
"'email' if your model is res.partner, or "
|
|
"'partner_id.email' if you're matching sale orders",
|
|
)
|
|
model_order = fields.Char(
|
|
"Order (model)",
|
|
help="Field(s) to order by, this mostly useful in conjunction "
|
|
"with 'Use 1st match'",
|
|
)
|
|
match_algorithm = fields.Selection(
|
|
_get_match_algorithms_sel,
|
|
"Match algorithm",
|
|
required=True,
|
|
help="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'",
|
|
)
|
|
delete_matching = fields.Boolean(
|
|
"Delete matches", help="Delete matched emails from server"
|
|
)
|
|
flag_nonmatching = fields.Boolean(
|
|
"Flag nonmatching",
|
|
default=True,
|
|
help="Flag emails in the server that don't 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",
|
|
)
|
|
domain = fields.Char(
|
|
"Domain", help="Fill in a search filter to narrow down objects to match"
|
|
)
|
|
msg_state = fields.Selection(
|
|
selection=[("sent", "Sent"), ("received", "Received")],
|
|
string="Message state",
|
|
default="received",
|
|
help="The state messages fetched from this folder should be "
|
|
"assigned in Odoo",
|
|
)
|
|
active = fields.Boolean("Active", default=True)
|
|
|
|
@api.multi
|
|
def get_algorithm(self):
|
|
return self._get_match_algorithms()[self.match_algorithm]()
|
|
|
|
@api.multi
|
|
def button_confirm_folder(self):
|
|
for this in self:
|
|
this.write({"state": "draft"})
|
|
if not this.active:
|
|
continue
|
|
connection = this.server_id.connect()
|
|
connection.select()
|
|
if connection.select(this.path)[0] != "OK":
|
|
raise ValidationError(_("Invalid folder %s!") % this.path)
|
|
connection.close()
|
|
this.write({"state": "done"})
|
|
|
|
@api.multi
|
|
def button_attach_mail_manually(self):
|
|
self.ensure_one()
|
|
return {
|
|
"type": "ir.actions.act_window",
|
|
"res_model": "fetchmail.attach.mail.manually",
|
|
"target": "new",
|
|
"context": dict(self.env.context, folder_id=self.id),
|
|
"view_type": "form",
|
|
"view_mode": "form",
|
|
}
|
|
|
|
@api.multi
|
|
def set_draft(self):
|
|
self.write({"state": "draft"})
|
|
return True
|
|
|
|
@api.multi
|
|
def get_msgids(self, connection, criteria):
|
|
"""Return imap ids of messages to process"""
|
|
self.ensure_one()
|
|
server = self.server_id
|
|
_logger.info(
|
|
"start checking for emails in folder %s on server %s",
|
|
self.path,
|
|
server.name,
|
|
)
|
|
if connection.select(self.path)[0] != "OK":
|
|
raise UserError(
|
|
_("Could not open mailbox %s on %s") % (self.path, server.name)
|
|
)
|
|
result, msgids = connection.search(None, criteria)
|
|
if result != "OK":
|
|
raise UserError(
|
|
_("Could not search mailbox %s on %s") % (self.path, server.name)
|
|
)
|
|
_logger.info(
|
|
"finished checking for emails in %s on server %s", self.path, server.name
|
|
)
|
|
return msgids
|
|
|
|
@api.multi
|
|
def fetch_msg(self, connection, msgid):
|
|
"""Select a single message from a folder."""
|
|
self.ensure_one()
|
|
server = self.server_id
|
|
result, msgdata = connection.fetch(msgid, "(RFC822)")
|
|
if result != "OK":
|
|
raise UserError(
|
|
_("Could not fetch %s in %s on %s") % (msgid, self.path, server.server)
|
|
)
|
|
message_org = msgdata[0][1] # rfc822 message source
|
|
mail_message = self.env["mail.thread"].message_parse(
|
|
message_org, save_original=server.original
|
|
)
|
|
return (mail_message, message_org)
|
|
|
|
@api.multi
|
|
def retrieve_imap_folder(self, connection):
|
|
"""Retrieve all mails for one IMAP folder."""
|
|
self.ensure_one()
|
|
msgids = self.get_msgids(connection, "UNDELETED")
|
|
match_algorithm = self.get_algorithm()
|
|
for msgid in msgids[0].split():
|
|
# We will accept exceptions for single messages
|
|
try:
|
|
self.env.cr.execute("savepoint apply_matching")
|
|
self.apply_matching(connection, msgid, match_algorithm)
|
|
self.env.cr.execute("release savepoint apply_matching")
|
|
except Exception:
|
|
self.env.cr.execute("rollback to savepoint apply_matching")
|
|
_logger.exception(
|
|
"Failed to fetch mail %s from %s", msgid, self.server_id.name
|
|
)
|
|
|
|
@api.multi
|
|
def fetch_mail(self):
|
|
"""Retrieve all mails for IMAP folders.
|
|
|
|
We will use a separate connection for each folder.
|
|
"""
|
|
for this in self:
|
|
if not this.active or this.state != "done":
|
|
continue
|
|
connection = None
|
|
try:
|
|
# New connection per folder
|
|
connection = this.server_id.connect()
|
|
this.retrieve_imap_folder(connection)
|
|
connection.close()
|
|
except Exception:
|
|
_logger.error(
|
|
_("General failure when trying to connect to %s server %s."),
|
|
this.server_id.type,
|
|
this.server_id.name,
|
|
exc_info=True,
|
|
)
|
|
finally:
|
|
if connection:
|
|
connection.logout()
|
|
|
|
@api.multi
|
|
def update_msg(self, connection, msgid, matched=True, flagged=False):
|
|
"""Update msg in imap folder depending on match and settings."""
|
|
if matched:
|
|
if self.delete_matching:
|
|
connection.store(msgid, "+FLAGS", "\\DELETED")
|
|
elif flagged and self.flag_nonmatching:
|
|
connection.store(msgid, "-FLAGS", "\\FLAGGED")
|
|
else:
|
|
if self.flag_nonmatching:
|
|
connection.store(msgid, "+FLAGS", "\\FLAGGED")
|
|
|
|
@api.multi
|
|
def apply_matching(self, connection, msgid, match_algorithm):
|
|
"""Return ids of objects matched"""
|
|
self.ensure_one()
|
|
mail_message, message_org = self.fetch_msg(connection, msgid)
|
|
if self.env["mail.message"].search(
|
|
[("message_id", "=", mail_message["message_id"])]
|
|
):
|
|
# Ignore mails that have been handled already
|
|
return
|
|
matches = match_algorithm.search_matches(self, mail_message)
|
|
matched = matches and (len(matches) == 1 or self.match_first)
|
|
if matched:
|
|
match_algorithm.handle_match(
|
|
connection, matches[0], self, mail_message, message_org, msgid
|
|
)
|
|
self.update_msg(connection, msgid, matched=matched)
|
|
|
|
@api.multi
|
|
def attach_mail(self, match_object, mail_message):
|
|
"""Attach mail to match_object."""
|
|
self.ensure_one()
|
|
partner = False
|
|
model_name = self.model_id.model
|
|
if model_name == "res.partner":
|
|
partner = match_object
|
|
elif "partner_id" in self.env[model_name]._fields:
|
|
partner = match_object.partner_id
|
|
attachments = []
|
|
if self.server_id.attach and mail_message.get("attachments"):
|
|
for attachment in mail_message["attachments"]:
|
|
# Attachment should at least have filename and data, but
|
|
# might have some extra element(s)
|
|
if len(attachment) < 2:
|
|
continue
|
|
fname, fcontent = attachment[:2]
|
|
if isinstance(fcontent, unicode):
|
|
fcontent = fcontent.encode("utf-8")
|
|
data_attach = {
|
|
"name": fname,
|
|
"datas": base64.b64encode(str(fcontent)),
|
|
"datas_fname": fname,
|
|
"description": _("Mail attachment"),
|
|
"res_model": model_name,
|
|
"res_id": match_object.id,
|
|
}
|
|
attachments.append(self.env["ir.attachment"].create(data_attach))
|
|
self.env["mail.message"].create(
|
|
{
|
|
"author_id": partner and partner.id or False,
|
|
"model": model_name,
|
|
"res_id": match_object.id,
|
|
"message_type": "email",
|
|
"body": mail_message.get("body"),
|
|
"subject": mail_message.get("subject"),
|
|
"email_from": mail_message.get("from"),
|
|
"date": mail_message.get("date"),
|
|
"message_id": mail_message.get("message_id"),
|
|
"attachment_ids": [(6, 0, [a.id for a in attachments])],
|
|
}
|
|
)
|