server-tools/fetchmail_attach_from_folder/models/fetchmail_server_folder.py

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])],
}
)