[MIG] fetchmail_attach_from_folder: Migration to 16.0
parent
b2400163fe
commit
9ca9d66aa7
|
@ -1,95 +1,133 @@
|
|||
=======================
|
||||
Email gateway - folders
|
||||
=======================
|
||||
|
||||
Adds the possibility to attach emails from a certain IMAP folder to objects,
|
||||
ie partners. Matching is done via several algorithms, ie email address, email
|
||||
address's domain or the original Odoo algorithm.
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:95f6645118da34dd962fa794f5fca9c8797579a9a585d80228407f4997e9ba91
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||
:alt: License: AGPL-3
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/server-tools/tree/16.0/fetchmail_attach_from_folder
|
||||
:alt: OCA/server-tools
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-fetchmail_attach_from_folder
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
Adds the possibility to attach emails from a certain IMAP folder to
|
||||
objects, ie partners. Matching is done via several algorithms, ie email
|
||||
address, email address's domain or the original Odoo algorithm.
|
||||
|
||||
This gives a simple possibility to archive emails in Odoo without a mail
|
||||
client integration.
|
||||
|
||||
**Table of contents**
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
In your fetchmail configuration, you'll find a new list field `Folders to
|
||||
monitor`. Add your folders here in IMAP notation (usually something like
|
||||
`INBOX.your_folder_name.your_subfolder_name`), choose a model to attach mails
|
||||
to and a matching algorithm to use.
|
||||
In your fetchmail configuration, you'll find a new list field
|
||||
``Folders to monitor``. Add your folders here in IMAP notation (usually
|
||||
something like ``INBOX.your_folder_name.your_subfolder_name``), choose a
|
||||
model to attach mails to and a matching algorithm to use.
|
||||
|
||||
Exact mailaddress
|
||||
-----------------
|
||||
|
||||
Fill in a field to search for the email address in `Field (model)`. For
|
||||
partners, this would be `email`. Also fill in the header field from the email
|
||||
to look at in `Field (email)`. If you want to match incoming mails from your
|
||||
customers, this would be `from`. You can also list header fields, so to match
|
||||
partners receiving this email, you might fill in `to,cc,bcc`.
|
||||
Fill in a field to search for the email address in ``Field (model)``.
|
||||
For partners, this would be ``email``. Also fill in the header field
|
||||
from the email to look at in ``Field (email)``. If you want to match
|
||||
incoming mails from your customers, this would be ``from``. You can also
|
||||
list header fields, so to match partners receiving this email, you might
|
||||
fill in ``to,cc,bcc``.
|
||||
|
||||
Domain of email addresses
|
||||
-------------------------
|
||||
|
||||
Match the domain of the email address(es) found in `Field (email)`. This would
|
||||
attach a mail to `test1@example.com` to a record with `Field (model)` set to
|
||||
`test2@example.com`. Given that this is a fuzzy match, you probably want to
|
||||
check `Use 1st match`, because otherwise nothing happens if multiple possible
|
||||
matches are found.
|
||||
Match the domain of the email address(es) found in ``Field (email)``.
|
||||
This would attach a mail to ``test1@example.com`` to a record with
|
||||
``Field (model)`` set to ``test2@example.com``. Given that this is a
|
||||
fuzzy match, you probably want to check ``Use 1st match``, because
|
||||
otherwise nothing happens if multiple possible matches are found.
|
||||
|
||||
Odoo standard
|
||||
-------------
|
||||
|
||||
This is stricly speaking no matching algorithm, but calls the model's standard
|
||||
action on new incoming mail, which is usually creating a new record.
|
||||
This is stricly speaking no matching algorithm, but calls the model's
|
||||
standard action on new incoming mail, which is usually creating a new
|
||||
record.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
A widespread configuration is to have a shared mailbox with several folders,
|
||||
i.e. one where users drop mails they want to attach to partners. Let this
|
||||
folder be called `From partners`. Then create a folder configuration for your
|
||||
server with path `"INBOX.From partners"` (note the quotes because of the space,
|
||||
this is server dependent). Choose model `Partners`, set `Field (model)` to
|
||||
`email` and `Field (email)` to `from`. In `Domain`, you could fill in
|
||||
`[('customer', '=', True)]` to be sure to only match customer records.
|
||||
|
||||
Now when your users drop mails into this folder, they will be fetched by Odoo
|
||||
and attached to the partner in question. After some testing, you might want to
|
||||
check `Delete matches` in your folder configuration so that this folder doesn't
|
||||
grow indefinitely.
|
||||
A widespread configuration is to have a shared mailbox with several
|
||||
folders, i.e. one where users drop mails they want to attach to
|
||||
partners. Let this folder be called ``From partners``. Then create a
|
||||
folder configuration for your server with path ``"INBOX.From partners"``
|
||||
(note the quotes because of the space, this is server dependent). Choose
|
||||
model ``Partners``, set ``Field (model)`` to ``email`` and
|
||||
``Field (email)`` to ``from``. In ``Domain``, you could fill in
|
||||
``[('customer', '=', True)]`` to be sure to only match customer records.
|
||||
|
||||
Now when your users drop mails into this folder, they will be fetched by
|
||||
Odoo and attached to the partner in question. After some testing, you
|
||||
might want to check ``Delete matches`` in your folder configuration so
|
||||
that this folder doesn't grow indefinitely.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues
|
||||
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
|
||||
check there if your issue has already been reported. If you spotted it first,
|
||||
help us smashing it by providing a detailed and welcomed feedback.
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/server-tools/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20fetchmail_attach_from_folder%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Do not contact contributors directly about support or help with technical issues.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Authors
|
||||
-------
|
||||
|
||||
* Therp BV
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Holger Brunn <hbrunn@therp.nl>
|
||||
* Ronald Portier <ronald@therp.nl>
|
||||
- Holger Brunn hbrunn@therp.nl
|
||||
- Ronald Portier ronald@therp.nl
|
||||
|
||||
Icon
|
||||
----
|
||||
|
||||
http://commons.wikimedia.org/wiki/File:Crystal_Clear_filesystem_folder_favorites.png
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: http://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: http://odoo-community.org
|
||||
Maintainers
|
||||
-----------
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit http://odoo-community.org.
|
||||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/16.0/fetchmail_attach_from_folder>`_ project on GitHub.
|
||||
|
||||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
{
|
||||
"name": "Email gateway - folders",
|
||||
"summary": "Attach mails in an IMAP folder to existing objects",
|
||||
"version": "10.0.1.1.0",
|
||||
"version": "16.0.1.1.0",
|
||||
"author": "Therp BV,Odoo Community Association (OCA)",
|
||||
"website": "https://github.com/OCA/server-tools",
|
||||
"license": "AGPL-3",
|
||||
"category": "Tools",
|
||||
"depends": ["fetchmail"],
|
||||
"depends": ["mail"],
|
||||
"data": [
|
||||
"views/fetchmail_server.xml",
|
||||
"wizard/attach_mail_manually.xml",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from . import base
|
||||
from . import email_exact
|
||||
from . import email_domain
|
||||
from . import odoo_standard
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
|
||||
|
||||
class Base(object):
|
||||
name = None # Name shown to the user
|
||||
|
||||
# Fields on fetchmail_server folder required for this algorithm
|
||||
required_fields = []
|
||||
|
||||
# Fields on fetchmail_server folder readonly for this algorithm
|
||||
readonly_fields = []
|
||||
|
||||
def search_matches(self, folder, mail_message):
|
||||
"""Returns recordset found for model with mail_message."""
|
||||
return []
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from .email_exact import EmailExact
|
||||
|
||||
|
@ -9,11 +9,9 @@ class EmailDomain(EmailExact):
|
|||
Beware of match_first here, this is most likely to get it wrong (gmail).
|
||||
"""
|
||||
|
||||
name = "Domain of email address"
|
||||
|
||||
def search_matches(self, folder, mail_message):
|
||||
"""Returns recordset of matching objects."""
|
||||
matches = super(EmailDomain, self).search_matches(folder, mail_message)
|
||||
matches = super().search_matches(folder, mail_message)
|
||||
if not matches:
|
||||
object_model = folder.env[folder.model_id.model]
|
||||
domains = []
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from odoo.tools.mail import email_split
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
@ -9,9 +9,6 @@ from .base import Base
|
|||
class EmailExact(Base):
|
||||
"""Search for exactly the mailadress as noted in the email"""
|
||||
|
||||
name = "Exact mailadress"
|
||||
required_fields = ["model_field", "mail_field"]
|
||||
|
||||
def _get_mailaddresses(self, folder, mail_message):
|
||||
mailaddresses = []
|
||||
fields = folder.mail_field.split(",")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
from .base import Base
|
||||
|
||||
|
@ -7,16 +7,6 @@ class OdooStandard(Base):
|
|||
"""No search at all. Use Odoo's standard mechanism to attach mails to
|
||||
mail.thread objects. Note that this algorithm always matches."""
|
||||
|
||||
name = "Odoo standard"
|
||||
readonly_fields = [
|
||||
"model_field",
|
||||
"mail_field",
|
||||
"match_first",
|
||||
"domain",
|
||||
"model_order",
|
||||
"flag_nonmatching",
|
||||
]
|
||||
|
||||
def search_matches(self, folder, mail_message):
|
||||
"""Always match. Duplicates will be fished out by message_id"""
|
||||
return [True]
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 Therp BV <https://therp.nl>.
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools.misc import UnquoteEvalContext
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -20,7 +15,6 @@ list_response_pattern = re.compile(
|
|||
class FetchmailServer(models.Model):
|
||||
_inherit = "fetchmail.server"
|
||||
|
||||
@api.multi
|
||||
def _compute_folders_available(self):
|
||||
"""Retrieve available folders from IMAP server."""
|
||||
|
||||
|
@ -40,7 +34,7 @@ class FetchmailServer(models.Model):
|
|||
continue
|
||||
folders_available = []
|
||||
for folder_entry in list_result[1]:
|
||||
folders_available.append(parse_list_response(folder_entry)[2])
|
||||
folders_available.append(parse_list_response(str(folder_entry))[2])
|
||||
this.folders_available = "\n".join(folders_available)
|
||||
connection.logout()
|
||||
|
||||
|
@ -54,71 +48,23 @@ class FetchmailServer(models.Model):
|
|||
context={"active_test": False},
|
||||
)
|
||||
object_id = fields.Many2one(required=False) # comodel_name='ir.model'
|
||||
type = fields.Selection(default="imap")
|
||||
server_type = fields.Selection(default="imap")
|
||||
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.",
|
||||
)
|
||||
|
||||
@api.onchange("type", "is_ssl", "object_id")
|
||||
@api.onchange("server_type", "is_ssl", "object_id")
|
||||
def onchange_server_type(self):
|
||||
super(FetchmailServer, self).onchange_server_type()
|
||||
result = super().onchange_server_type()
|
||||
self.state = "draft"
|
||||
return result
|
||||
|
||||
@api.multi
|
||||
def fetch_mail(self):
|
||||
result = True
|
||||
for this in self:
|
||||
if not this.folders_only:
|
||||
super(FetchmailServer, this).fetch_mail()
|
||||
result = result and super(FetchmailServer, this).fetch_mail()
|
||||
this.folder_ids.fetch_mail()
|
||||
|
||||
def fields_view_get(
|
||||
self, view_id=None, view_type="form", toolbar=False, submenu=False
|
||||
):
|
||||
"""Set modifiers for form fields in folder_ids depending on algorithm.
|
||||
|
||||
A field will be readonly and/or required if this is specified in the
|
||||
algorithm.
|
||||
"""
|
||||
result = super(FetchmailServer, self).fields_view_get(
|
||||
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
|
||||
)
|
||||
if view_type == "form":
|
||||
view = etree.fromstring(
|
||||
result["fields"]["folder_ids"]["views"]["form"]["arch"]
|
||||
)
|
||||
modifiers = {}
|
||||
docstr = ""
|
||||
folder_model = self.env["fetchmail.server.folder"]
|
||||
match_algorithms = folder_model._get_match_algorithms()
|
||||
for algorithm in match_algorithms.itervalues():
|
||||
for modifier in ["required", "readonly"]:
|
||||
for field in getattr(algorithm, modifier + "_fields"):
|
||||
modifiers.setdefault(field, {})
|
||||
modifiers[field].setdefault(modifier, [])
|
||||
if modifiers[field][modifier]:
|
||||
modifiers[field][modifier].insert(0, "|")
|
||||
modifiers[field][modifier].append(
|
||||
("match_algorithm", "==", algorithm.__name__)
|
||||
)
|
||||
docstr += _(algorithm.name) + "\n" + _(algorithm.__doc__) + "\n\n"
|
||||
for field in view.xpath("//field"):
|
||||
if field.tag == "field" and field.get("name") in modifiers:
|
||||
patched_modifiers = (
|
||||
field.attrib["modifiers"]
|
||||
.replace("false", "False")
|
||||
.replace("true", "True")
|
||||
)
|
||||
original_dict = safe_eval(
|
||||
patched_modifiers, UnquoteEvalContext({}), nocopy=True
|
||||
)
|
||||
modifier_dict = modifiers[field.attrib["name"]]
|
||||
combined_dict = dict(original_dict, **modifier_dict)
|
||||
field.set("modifiers", json.dumps(combined_dict))
|
||||
if field.tag == "field" and field.get("name") == "match_algorithm":
|
||||
field.set("help", docstr)
|
||||
result["fields"]["folder_ids"]["views"]["form"]["arch"] = etree.tostring(
|
||||
view
|
||||
)
|
||||
return result
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Copyright - 2013-2018 Therp BV <https://therp.nl>.
|
||||
# Copyright - 2013-2024 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 import _, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
|
||||
from .. import match_algorithm
|
||||
|
@ -16,28 +16,8 @@ class FetchmailServerFolder(models.Model):
|
|||
_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")
|
||||
server_id = fields.Many2one("fetchmail.server")
|
||||
sequence = fields.Integer()
|
||||
state = fields.Selection(
|
||||
[("draft", "Not Confirmed"), ("done", "Confirmed")],
|
||||
string="Status",
|
||||
|
@ -47,18 +27,19 @@ class FetchmailServerFolder(models.Model):
|
|||
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"
|
||||
comodel_name="ir.model",
|
||||
required=True,
|
||||
ondelete="cascade",
|
||||
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="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",
|
||||
|
@ -69,21 +50,23 @@ class FetchmailServerFolder(models.Model):
|
|||
"with 'Use 1st match'",
|
||||
)
|
||||
match_algorithm = fields.Selection(
|
||||
_get_match_algorithms_sel,
|
||||
"Match algorithm",
|
||||
selection=[
|
||||
("odoo_standard", "Odoo standard"),
|
||||
("email_domain", "Domain of email address"),
|
||||
("email_exact", "Exact mailadress"),
|
||||
],
|
||||
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'",
|
||||
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",
|
||||
)
|
||||
|
@ -92,26 +75,34 @@ class FetchmailServerFolder(models.Model):
|
|||
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"
|
||||
)
|
||||
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",
|
||||
default="received",
|
||||
help="The state messages fetched from this folder should be "
|
||||
"assigned in Odoo",
|
||||
help="The state messages fetched from this folder should be assigned in Odoo",
|
||||
)
|
||||
active = fields.Boolean("Active", default=True)
|
||||
active = fields.Boolean(default=True)
|
||||
|
||||
@api.multi
|
||||
def get_algorithm(self):
|
||||
return self._get_match_algorithms()[self.match_algorithm]()
|
||||
"""Translate algorithm code to implementation class.
|
||||
|
||||
We used to load this dynamically, but having it more or less hardcoded
|
||||
allows to adapt the UI to the selected algorithm, withouth needing
|
||||
the (deprecated) fields_view_get trickery we used in the past.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.match_algorithm == "odoo_standard":
|
||||
return match_algorithm.odoo_standard.OdooStandard
|
||||
if self.match_algorithm == "email_domain":
|
||||
return match_algorithm.email_domain.EmailDomain
|
||||
if self.match_algorithm == "email_exact":
|
||||
return match_algorithm.email_exact.EmailExact
|
||||
return None
|
||||
|
||||
@api.multi
|
||||
def button_confirm_folder(self):
|
||||
self.write({"state": "draft"})
|
||||
for this in self:
|
||||
this.write({"state": "draft"})
|
||||
if not this.active:
|
||||
continue
|
||||
connection = this.server_id.connect()
|
||||
|
@ -121,7 +112,6 @@ class FetchmailServerFolder(models.Model):
|
|||
connection.close()
|
||||
this.write({"state": "done"})
|
||||
|
||||
@api.multi
|
||||
def button_attach_mail_manually(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
|
@ -133,52 +123,50 @@ class FetchmailServerFolder(models.Model):
|
|||
"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,
|
||||
"start checking for emails in folder %(folder)s on server %(server)s",
|
||||
{"folder": self.path, "server": server.name},
|
||||
)
|
||||
if connection.select(self.path)[0] != "OK":
|
||||
raise UserError(
|
||||
_("Could not open mailbox %s on %s") % (self.path, server.name)
|
||||
_("Could not open folder %(folder)s on server %(server)s")
|
||||
% {"folder": self.path, "server": server.name}
|
||||
)
|
||||
result, msgids = connection.search(None, criteria)
|
||||
if result != "OK":
|
||||
raise UserError(
|
||||
_("Could not search mailbox %s on %s") % (self.path, server.name)
|
||||
_("Could not search folder %(folder)s on server %(server)s")
|
||||
% {"folder": self.path, "server": server.name}
|
||||
)
|
||||
_logger.info(
|
||||
"finished checking for emails in %s on server %s", self.path, server.name
|
||||
"finished checking for emails in folder %(folder)s on server %(server)s",
|
||||
{"folder": self.path, "server": 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)
|
||||
_("Could not fetch %(msgid)s in folder %(folder)s on server %(server)s")
|
||||
% {"msgid": msgid, "folder": self.path, "server": self.server_id.name}
|
||||
)
|
||||
message_org = msgdata[0][1] # rfc822 message source
|
||||
mail_message = self.env["mail.thread"].message_parse(
|
||||
message_org, save_original=server.original
|
||||
message_org, save_original=self.server_id.original
|
||||
)
|
||||
return (mail_message, message_org)
|
||||
|
||||
@api.multi
|
||||
def retrieve_imap_folder(self, connection):
|
||||
"""Retrieve all mails for one IMAP folder."""
|
||||
self.ensure_one()
|
||||
|
@ -193,10 +181,10 @@ class FetchmailServerFolder(models.Model):
|
|||
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
|
||||
"Failed to fetch mail %(msgid)s from server %(server)s",
|
||||
{"msgid": msgid, "server": self.server_id.name},
|
||||
)
|
||||
|
||||
@api.multi
|
||||
def fetch_mail(self):
|
||||
"""Retrieve all mails for IMAP folders.
|
||||
|
||||
|
@ -213,16 +201,20 @@ class FetchmailServerFolder(models.Model):
|
|||
connection.close()
|
||||
except Exception:
|
||||
_logger.error(
|
||||
_("General failure when trying to connect to %s server %s."),
|
||||
this.server_id.type,
|
||||
this.server_id.name,
|
||||
(
|
||||
"General failure when trying to connect to"
|
||||
" %(server_type)s server %(server)s."
|
||||
),
|
||||
{
|
||||
"server_type": this.server_id.server_type,
|
||||
"server": 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:
|
||||
|
@ -234,7 +226,6 @@ class FetchmailServerFolder(models.Model):
|
|||
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()
|
||||
|
@ -252,7 +243,6 @@ class FetchmailServerFolder(models.Model):
|
|||
)
|
||||
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()
|
||||
|
@ -270,11 +260,9 @@ class FetchmailServerFolder(models.Model):
|
|||
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": base64.b64encode(fcontent),
|
||||
"datas_fname": fname,
|
||||
"description": _("Mail attachment"),
|
||||
"res_model": model_name,
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
In your fetchmail configuration, you'll find a new list field `Folders to
|
||||
monitor`. Add your folders here in IMAP notation (usually something like
|
||||
`INBOX.your_folder_name.your_subfolder_name`), choose a model to attach mails
|
||||
to and a matching algorithm to use.
|
||||
|
||||
Exact mailaddress
|
||||
-----------------
|
||||
|
||||
Fill in a field to search for the email address in `Field (model)`. For
|
||||
partners, this would be `email`. Also fill in the header field from the email
|
||||
to look at in `Field (email)`. If you want to match incoming mails from your
|
||||
customers, this would be `from`. You can also list header fields, so to match
|
||||
partners receiving this email, you might fill in `to,cc,bcc`.
|
||||
|
||||
Domain of email addresses
|
||||
-------------------------
|
||||
|
||||
Match the domain of the email address(es) found in `Field (email)`. This would
|
||||
attach a mail to `test1@example.com` to a record with `Field (model)` set to
|
||||
`test2@example.com`. Given that this is a fuzzy match, you probably want to
|
||||
check `Use 1st match`, because otherwise nothing happens if multiple possible
|
||||
matches are found.
|
||||
|
||||
Odoo standard
|
||||
-------------
|
||||
|
||||
This is stricly speaking no matching algorithm, but calls the model's standard
|
||||
action on new incoming mail, which is usually creating a new record.
|
|
@ -0,0 +1,2 @@
|
|||
- Holger Brunn <hbrunn@therp.nl>
|
||||
- Ronald Portier <ronald@therp.nl>
|
|
@ -0,0 +1,6 @@
|
|||
Adds the possibility to attach emails from a certain IMAP folder to objects,
|
||||
ie partners. Matching is done via several algorithms, ie email address, email
|
||||
address's domain or the original Odoo algorithm.
|
||||
|
||||
This gives a simple possibility to archive emails in Odoo without a mail
|
||||
client integration.
|
|
@ -0,0 +1,12 @@
|
|||
A widespread configuration is to have a shared mailbox with several folders,
|
||||
i.e. one where users drop mails they want to attach to partners. Let this
|
||||
folder be called `From partners`. Then create a folder configuration for your
|
||||
server with path `"INBOX.From partners"` (note the quotes because of the space,
|
||||
this is server dependent). Choose model `Partners`, set `Field (model)` to
|
||||
`email` and `Field (email)` to `from`. In `Domain`, you could fill in
|
||||
`[('customer', '=', True)]` to be sure to only match customer records.
|
||||
|
||||
Now when your users drop mails into this folder, they will be fetched by Odoo
|
||||
and attached to the partner in question. After some testing, you might want to
|
||||
check `Delete matches` in your folder configuration so that this folder doesn't
|
||||
grow indefinitely.
|
|
@ -146,12 +146,3 @@ class TestMatchAlgorithms(TransactionCase):
|
|||
folder.match_algorithm = "EmailDomain"
|
||||
connection = MockConnection()
|
||||
folder.retrieve_imap_folder(connection)
|
||||
|
||||
def test_field_view_get(self):
|
||||
"""For the moment just check execution withouth errors."""
|
||||
server_model = self.env["fetchmail.server"]
|
||||
view = server_model.fields_view_get()
|
||||
self.assertTrue(view)
|
||||
self.assertIn(
|
||||
"match_algorithm", view["fields"]["folder_ids"]["views"]["form"]["arch"]
|
||||
)
|
||||
|
|
|
@ -4,95 +4,104 @@
|
|||
<record model="ir.ui.view" id="view_email_server_form">
|
||||
<field name="name">fetchmail.server.form</field>
|
||||
<field name="model">fetchmail.server</field>
|
||||
<field name="inherit_id" ref="fetchmail.view_email_server_form" />
|
||||
<field name="inherit_id" ref="mail.view_email_server_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="object_id" position="attributes">
|
||||
<attribute
|
||||
name="attrs"
|
||||
>{'required': [('type', '!=', 'imap')]}</attribute>
|
||||
>{'required': [('server_type', '!=', 'imap')]}</attribute>
|
||||
</field>
|
||||
<field name="type" position="after">
|
||||
<field name="server_type" position="after">
|
||||
<field name="folders_only" />
|
||||
</field>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page
|
||||
string="Folders to monitor"
|
||||
attrs="{'invisible': [('type','!=','imap')]}"
|
||||
attrs="{'invisible': [('server_type','!=','imap')]}"
|
||||
>
|
||||
<group>
|
||||
<field name="folder_ids" nolabel="1">
|
||||
<tree colors="gray:active==False">
|
||||
<field name="active" invisible="True" />
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="path" />
|
||||
<field name="folder_ids" nolabel="1">
|
||||
<tree decoration-muted="active == False">
|
||||
<field name="active" invisible="True" />
|
||||
<field name="sequence" widget="handle" />
|
||||
<field name="path" />
|
||||
<field name="model_id" />
|
||||
<field name="match_algorithm" />
|
||||
<field name="model_field" />
|
||||
<field name="mail_field" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="state" invisible="1" />
|
||||
<header>
|
||||
<button
|
||||
type="object"
|
||||
name="button_confirm_folder"
|
||||
string="Test & Confirm"
|
||||
states="draft"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="button_attach_mail_manually"
|
||||
string="Attach mail manually"
|
||||
states="done"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="fetch_mail"
|
||||
string="Fetch folder now"
|
||||
states="done"
|
||||
/>
|
||||
<button
|
||||
string="Reset Confirmation"
|
||||
type="object"
|
||||
name="set_draft"
|
||||
states="done"
|
||||
/>
|
||||
</header>
|
||||
<group>
|
||||
<field name="path" placeholder="INBOX.subfolder1" />
|
||||
<field name="model_id" />
|
||||
<field name="model_field" />
|
||||
<field name="match_algorithm" />
|
||||
<field name="mail_field" />
|
||||
<field name="state" />
|
||||
</tree>
|
||||
<form>
|
||||
<field name="state" invisible="1" />
|
||||
<header>
|
||||
<button
|
||||
type="object"
|
||||
name="button_confirm_folder"
|
||||
string="Test & Confirm"
|
||||
states="draft"
|
||||
<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'])]}"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="button_attach_mail_manually"
|
||||
string="Attach mail manually"
|
||||
states="done"
|
||||
<field
|
||||
name="mail_field"
|
||||
placeholder="to,from"
|
||||
attrs="{'required':
|
||||
[('match_algorithm','in',['email_exact','email_domain'])]}"
|
||||
/>
|
||||
<button
|
||||
type="object"
|
||||
name="fetch_mail"
|
||||
string="Fetch folder now"
|
||||
states="done"
|
||||
<field name="match_first" />
|
||||
<field
|
||||
name="domain"
|
||||
placeholder="[('state', '=', 'open')]"
|
||||
/>
|
||||
<button
|
||||
string="Reset Confirmation"
|
||||
type="object"
|
||||
name="set_draft"
|
||||
states="done"
|
||||
<field
|
||||
name="model_order"
|
||||
placeholder="name asc"
|
||||
attrs="{'readonly':
|
||||
[('match_first','==',False)],
|
||||
'required':
|
||||
[('match_first','==',True)]}"
|
||||
/>
|
||||
</header>
|
||||
<group>
|
||||
<group>
|
||||
<field
|
||||
name="path"
|
||||
placeholder="INBOX.subfolder1"
|
||||
/>
|
||||
<field name="model_id" />
|
||||
<field name="model_field" placeholder="email" />
|
||||
<field name="match_algorithm" />
|
||||
<field
|
||||
name="mail_field"
|
||||
placeholder="to,from"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" />
|
||||
<field name="delete_matching" />
|
||||
<field name="flag_nonmatching" />
|
||||
<field name="match_first" />
|
||||
<field name="msg_state" />
|
||||
<field
|
||||
name="model_order"
|
||||
attrs="{'readonly': [('match_first','==',False)], 'required': [('match_first','==',True)]}"
|
||||
placeholder="name asc,type desc"
|
||||
/>
|
||||
<field
|
||||
name="domain"
|
||||
placeholder="[('state', '=', 'open')]"
|
||||
/>
|
||||
</group>
|
||||
<field name="flag_nonmatching" />
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
</group>
|
||||
<group>
|
||||
<field name="active" />
|
||||
<field name="delete_matching" />
|
||||
<field name="msg_state" />
|
||||
</group>
|
||||
</group>
|
||||
</form>
|
||||
</field>
|
||||
<separator string="Folders available on server" colspan="6" />
|
||||
<field name="folders_available" />
|
||||
</page>
|
||||
|
|
|
@ -11,7 +11,7 @@ class AttachMailManually(models.TransientModel):
|
|||
_name = "fetchmail.attach.mail.manually"
|
||||
|
||||
name = fields.Char()
|
||||
folder_id = fields.Many2one("fetchmail.server.folder", "Folder", readonly=True)
|
||||
folder_id = fields.Many2one(comodel_name="fetchmail.server.folder", readonly=True)
|
||||
mail_ids = fields.One2many(
|
||||
"fetchmail.attach.mail.manually.mail", "wizard_id", "Emails"
|
||||
)
|
||||
|
@ -49,7 +49,6 @@ class AttachMailManually(models.TransientModel):
|
|||
connection.close()
|
||||
return defaults
|
||||
|
||||
@api.multi
|
||||
def attach_mails(self):
|
||||
self.ensure_one()
|
||||
folder = self.folder_id
|
||||
|
@ -72,6 +71,7 @@ class AttachMailManually(models.TransientModel):
|
|||
def fields_view_get(
|
||||
self, view_id=None, view_type="form", toolbar=False, submenu=False
|
||||
):
|
||||
# TODO: Change or replace this...
|
||||
result = super(AttachMailManually, self).fields_view_get(
|
||||
view_id=view_id, view_type=view_type, toolbar=toolbar, submenu=submenu
|
||||
)
|
||||
|
@ -92,11 +92,10 @@ class AttachMailManuallyMail(models.TransientModel):
|
|||
|
||||
wizard_id = fields.Many2one("fetchmail.attach.mail.manually", readonly=True)
|
||||
msgid = fields.Char("Message id", readonly=True)
|
||||
subject = fields.Char("Subject", readonly=True)
|
||||
date = fields.Datetime("Date", readonly=True)
|
||||
subject = fields.Char(readonly=True)
|
||||
date = fields.Datetime(readonly=True)
|
||||
email_from = fields.Char("From", readonly=True)
|
||||
body = fields.Html("Body", readonly=True)
|
||||
body = fields.Html(readonly=True)
|
||||
object_id = fields.Reference(
|
||||
lambda self: [(m.model, m.name) for m in self.env["ir.model"].search([])],
|
||||
string="Object",
|
||||
)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
__import__('pkg_resources').declare_namespace(__name__)
|
|
@ -1 +0,0 @@
|
|||
__import__('pkg_resources').declare_namespace(__name__)
|
Loading…
Reference in New Issue