diff --git a/fetchmail_attach_from_folder/__init__.py b/fetchmail_attach_from_folder/__init__.py index 67e1e2b4c..547f477e7 100644 --- a/fetchmail_attach_from_folder/__init__.py +++ b/fetchmail_attach_from_folder/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import match_algorithm diff --git a/fetchmail_attach_from_folder/__manifest__.py b/fetchmail_attach_from_folder/__manifest__.py index 232161dea..7a5894224 100644 --- a/fetchmail_attach_from_folder/__manifest__.py +++ b/fetchmail_attach_from_folder/__manifest__.py @@ -1,20 +1,19 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # 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', - 'author': 'Therp BV,Odoo Community Association (OCA)', - 'website': 'http://www.therp.nl', - 'license': 'AGPL-3', - 'category': 'Tools', - 'depends': ['fetchmail'], - 'data': [ - 'views/fetchmail_server.xml', - 'wizard/attach_mail_manually.xml', - 'security/ir.model.access.csv', + "name": "Email gateway - folders", + "summary": "Attach mails in an IMAP folder to existing objects", + "version": "10.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"], + "data": [ + "views/fetchmail_server.xml", + "wizard/attach_mail_manually.xml", + "security/ir.model.access.csv", ], - 'installable': True, - 'auto_install': False, + "installable": True, + "auto_install": False, } diff --git a/fetchmail_attach_from_folder/match_algorithm/__init__.py b/fetchmail_attach_from_folder/match_algorithm/__init__.py index 0da5325ca..0f87298ac 100644 --- a/fetchmail_attach_from_folder/match_algorithm/__init__.py +++ b/fetchmail_attach_from_folder/match_algorithm/__init__.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import base diff --git a/fetchmail_attach_from_folder/match_algorithm/base.py b/fetchmail_attach_from_folder/match_algorithm/base.py index 0b823d0e4..127a405d4 100644 --- a/fetchmail_attach_from_folder/match_algorithm/base.py +++ b/fetchmail_attach_from_folder/match_algorithm/base.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). @@ -17,7 +16,7 @@ class Base(object): return [] def handle_match( - self, connection, match_object, folder, - mail_message, mail_message_org, msgid): + self, connection, match_object, folder, mail_message, mail_message_org, msgid + ): """Do whatever it takes to handle a match""" folder.attach_mail(match_object, mail_message) diff --git a/fetchmail_attach_from_folder/match_algorithm/email_domain.py b/fetchmail_attach_from_folder/match_algorithm/email_domain.py index 4e8d15274..b6d463a34 100644 --- a/fetchmail_attach_from_folder/match_algorithm/email_domain.py +++ b/fetchmail_attach_from_folder/match_algorithm/email_domain.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from .email_exact import EmailExact @@ -9,7 +8,8 @@ class EmailDomain(EmailExact): Beware of match_first here, this is most likely to get it wrong (gmail). """ - name = 'Domain of email address' + + name = "Domain of email address" def search_matches(self, folder, mail_message): """Returns recordset of matching objects.""" @@ -18,11 +18,14 @@ class EmailDomain(EmailExact): object_model = folder.env[folder.model_id.model] domains = [] for addr in self._get_mailaddresses(folder, mail_message): - domains.append(addr.split('@')[-1]) + domains.append(addr.split("@")[-1]) matches = object_model.search( self._get_mailaddress_search_domain( - folder, mail_message, - operator='like', - values=['%@' + domain for domain in set(domains)]), - order=folder.model_order) + folder, + mail_message, + operator="like", + values=["%@" + domain for domain in set(domains)], + ), + order=folder.model_order, + ) return matches diff --git a/fetchmail_attach_from_folder/match_algorithm/email_exact.py b/fetchmail_attach_from_folder/match_algorithm/email_exact.py index 8ee08b271..77c739a00 100644 --- a/fetchmail_attach_from_folder/match_algorithm/email_exact.py +++ b/fetchmail_attach_from_folder/match_algorithm/email_exact.py @@ -1,39 +1,40 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -from odoo.tools.safe_eval import safe_eval from odoo.tools.mail import email_split +from odoo.tools.safe_eval import safe_eval 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'] + + name = "Exact mailadress" + required_fields = ["model_field", "mail_field"] def _get_mailaddresses(self, folder, mail_message): mailaddresses = [] - fields = folder.mail_field.split(',') + fields = folder.mail_field.split(",") for field in fields: if field in mail_message: mailaddresses += email_split(mail_message[field]) return [addr.lower() for addr in mailaddresses] def _get_mailaddress_search_domain( - self, folder, mail_message, operator='=', values=None): - mailaddresses = values or self._get_mailaddresses( - folder, mail_message) + self, folder, mail_message, operator="=", values=None + ): + mailaddresses = values or self._get_mailaddresses(folder, mail_message) if not mailaddresses: - return [(0, '=', 1)] - search_domain = ((['|'] * (len(mailaddresses) - 1)) + [ - (folder.model_field, operator, addr) for addr in mailaddresses] + - safe_eval(folder.domain or '[]')) + return [(0, "=", 1)] + search_domain = ( + (["|"] * (len(mailaddresses) - 1)) + + [(folder.model_field, operator, addr) for addr in mailaddresses] + + safe_eval(folder.domain or "[]") + ) return search_domain def search_matches(self, folder, mail_message): """Returns recordset of matching objects.""" object_model = folder.env[folder.model_id.model] - search_domain = self._get_mailaddress_search_domain( - folder, mail_message) + search_domain = self._get_mailaddress_search_domain(folder, mail_message) return object_model.search(search_domain, order=folder.model_order) diff --git a/fetchmail_attach_from_folder/match_algorithm/odoo_standard.py b/fetchmail_attach_from_folder/match_algorithm/odoo_standard.py index 0ae48592f..3705080b6 100644 --- a/fetchmail_attach_from_folder/match_algorithm/odoo_standard.py +++ b/fetchmail_attach_from_folder/match_algorithm/odoo_standard.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from .base import Base @@ -7,14 +6,15 @@ from .base import Base 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' + + name = "Odoo standard" readonly_fields = [ - 'model_field', - 'mail_field', - 'match_first', - 'domain', - 'model_order', - 'flag_nonmatching', + "model_field", + "mail_field", + "match_first", + "domain", + "model_order", + "flag_nonmatching", ] def search_matches(self, folder, mail_message): @@ -22,10 +22,12 @@ class OdooStandard(Base): return [True] def handle_match( - self, connection, match_object, folder, - mail_message, mail_message_org, msgid): - thread_model = folder.env['mail.thread'] + self, connection, match_object, folder, mail_message, mail_message_org, msgid + ): + thread_model = folder.env["mail.thread"] thread_model.message_process( - folder.model_id.model, mail_message_org, + folder.model_id.model, + mail_message_org, save_original=folder.server_id.original, - strip_attachments=(not folder.server_id.attach)) + strip_attachments=(not folder.server_id.attach), + ) diff --git a/fetchmail_attach_from_folder/models/__init__.py b/fetchmail_attach_from_folder/models/__init__.py index a562a0898..0340cfd07 100644 --- a/fetchmail_attach_from_folder/models/__init__.py +++ b/fetchmail_attach_from_folder/models/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import fetchmail_server diff --git a/fetchmail_attach_from_folder/models/fetchmail_server.py b/fetchmail_attach_from_folder/models/fetchmail_server.py index 701719766..30afefeb7 100644 --- a/fetchmail_attach_from_folder/models/fetchmail_server.py +++ b/fetchmail_attach_from_folder/models/fetchmail_server.py @@ -1,69 +1,70 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +import json import logging import re -import json + from lxml import etree from odoo import _, api, fields, models -from odoo.tools.safe_eval import safe_eval from odoo.tools.misc import UnquoteEvalContext - +from odoo.tools.safe_eval import safe_eval _logger = logging.getLogger(__name__) list_response_pattern = re.compile( - r'\((?P.*?)\) "(?P.*)" (?P.*)') + r'\((?P.*?)\) "(?P.*)" (?P.*)' +) class FetchmailServer(models.Model): - _inherit = 'fetchmail.server' + _inherit = "fetchmail.server" @api.multi def _compute_folders_available(self): """Retrieve available folders from IMAP server.""" + def parse_list_response(line): - flags, delimiter, mailbox_name = \ - list_response_pattern.match(line).groups() + flags, delimiter, mailbox_name = list_response_pattern.match(line).groups() mailbox_name = mailbox_name.strip('"') return (flags, delimiter, mailbox_name) for this in self: - if this.state != 'done': - this.folders_available = _('Confirm connection first.') + if this.state != "done": + this.folders_available = _("Confirm connection first.") continue connection = this.connect() list_result = connection.list() - if list_result[0] != 'OK': - this.folders_available = _('Unable to retrieve folders.') + if list_result[0] != "OK": + this.folders_available = _("Unable to retrieve folders.") continue folders_available = [] for folder_entry in list_result[1]: folders_available.append(parse_list_response(folder_entry)[2]) - this.folders_available = '\n'.join(folders_available) + this.folders_available = "\n".join(folders_available) connection.logout() folders_available = fields.Text( - string='Available folders', - compute='_compute_folders_available', - readonly=True) + string="Available folders", compute="_compute_folders_available", readonly=True + ) folder_ids = fields.One2many( - comodel_name='fetchmail.server.folder', - inverse_name='server_id', - string='Folders', - context={'active_test': False}) + comodel_name="fetchmail.server.folder", + inverse_name="server_id", + string="Folders", + context={"active_test": False}, + ) object_id = fields.Many2one(required=False) # comodel_name='ir.model' - type = fields.Selection(default='imap') + type = fields.Selection(default="imap") folders_only = fields.Boolean( - string='Only folders, not inbox', + string="Only folders, not inbox", help="Check this field to leave imap inbox alone" - " and only retrieve mail from configured folders.") + " and only retrieve mail from configured folders.", + ) - @api.onchange('type', 'is_ssl', 'object_id') + @api.onchange("type", "is_ssl", "object_id") def onchange_server_type(self): super(FetchmailServer, self).onchange_server_type() - self.state = 'draft' + self.state = "draft" @api.multi def fetch_mail(self): @@ -73,48 +74,51 @@ class FetchmailServer(models.Model): this.folder_ids.fetch_mail() def fields_view_get( - self, view_id=None, view_type='form', - toolbar=False, submenu=False): + 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_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']) + result["fields"]["folder_ids"]["views"]["form"]["arch"] + ) modifiers = {} - docstr = '' - folder_model = self.env['fetchmail.server.folder'] + 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'): + 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].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') + ("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']] + 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) + 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 diff --git a/fetchmail_attach_from_folder/models/fetchmail_server_folder.py b/fetchmail_attach_from_folder/models/fetchmail_server_folder.py index bc40eb10b..293b955b4 100644 --- a/fetchmail_attach_from_folder/models/fetchmail_server_folder.py +++ b/fetchmail_attach_from_folder/models/fetchmail_server_folder.py @@ -1,32 +1,33 @@ -# -*- coding: utf-8 -*- # Copyright - 2013-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). import base64 import logging -from odoo import _, api, models, fields +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' + _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 dict([(cls.__name__, cls) - for cls in get_all_subclasses( - match_algorithm.base.Base)]) + 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 = [] @@ -35,64 +36,73 @@ class FetchmailServerFolder(models.Model): 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', + 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') + default="draft", + ) path = fields.Char( - 'Path', + "Path", required=True, help="The path to your mail folder." - " Typically would be something like 'INBOX.myfolder'") + " Typically would be something like 'INBOX.myfolder'", + ) model_id = fields.Many2one( - 'ir.model', 'Model', required=True, - help='The model to attach emails to') + "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' + "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") + "'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'") + "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.') + "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'") + "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') + "Delete matches", help="Delete matched emails from server" + ) flag_nonmatching = fields.Boolean( - 'Flag nonmatching', + "Flag nonmatching", default=True, - help="Flag emails in the server that don't match any object in Odoo") + 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') + "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') + "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) + 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): @@ -101,31 +111,31 @@ class FetchmailServerFolder(models.Model): @api.multi def button_confirm_folder(self): for this in self: - this.write({'state': 'draft'}) + 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) + if connection.select(this.path)[0] != "OK": + raise ValidationError(_("Invalid folder %s!") % this.path) connection.close() - this.write({'state': 'done'}) + 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'} + "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'}) + self.write({"state": "draft"}) return True @api.multi @@ -134,20 +144,22 @@ class FetchmailServerFolder(models.Model): 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)) + "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)) + 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) + "finished checking for emails in %s on server %s", self.path, server.name + ) return msgids @api.multi @@ -155,33 +167,34 @@ class FetchmailServerFolder(models.Model): """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)) + 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) + 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') + 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.env.cr.execute("savepoint apply_matching") self.apply_matching(connection, msgid, match_algorithm) - self.env.cr.execute('release savepoint apply_matching') + self.env.cr.execute("release savepoint apply_matching") except Exception: - self.env.cr.execute('rollback to savepoint apply_matching') + 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 %s from %s", msgid, self.server_id.name + ) @api.multi def fetch_mail(self): @@ -190,7 +203,7 @@ class FetchmailServerFolder(models.Model): We will use a separate connection for each folder. """ for this in self: - if not this.active or this.state != 'done': + if not this.active or this.state != "done": continue connection = None try: @@ -199,9 +212,12 @@ class FetchmailServerFolder(models.Model): 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) + _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() @@ -211,29 +227,29 @@ class FetchmailServerFolder(models.Model): """Update msg in imap folder depending on match and settings.""" if matched: if self.delete_matching: - connection.store(msgid, '+FLAGS', '\\DELETED') + connection.store(msgid, "+FLAGS", "\\DELETED") elif flagged and self.flag_nonmatching: - connection.store(msgid, '-FLAGS', '\\FLAGGED') + connection.store(msgid, "-FLAGS", "\\FLAGGED") else: if self.flag_nonmatching: - connection.store(msgid, '+FLAGS', '\\FLAGGED') + 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'])]): + 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) + connection, matches[0], self, mail_message, message_org, msgid + ) self.update_msg(connection, msgid, matched=matched) @api.multi @@ -242,37 +258,40 @@ class FetchmailServerFolder(models.Model): self.ensure_one() partner = False model_name = self.model_id.model - if model_name == 'res.partner': + if model_name == "res.partner": partner = match_object - elif 'partner_id' in self.env[model_name]._fields: + 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']: + 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') + 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])]}) + "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])], + } + ) diff --git a/fetchmail_attach_from_folder/tests/__init__.py b/fetchmail_attach_from_folder/tests/__init__.py index c2f51343b..937f17cc8 100644 --- a/fetchmail_attach_from_folder/tests/__init__.py +++ b/fetchmail_attach_from_folder/tests/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright - 2015-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from . import test_match_algorithms diff --git a/fetchmail_attach_from_folder/tests/test_match_algorithms.py b/fetchmail_attach_from_folder/tests/test_match_algorithms.py index 28cbfbf8d..9cfa6810e 100644 --- a/fetchmail_attach_from_folder/tests/test_match_algorithms.py +++ b/fetchmail_attach_from_folder/tests/test_match_algorithms.py @@ -1,108 +1,118 @@ -# -*- coding: utf-8 -*- # Copyright - 2015-2018 Therp BV . # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import models from odoo.tests.common import TransactionCase -from ..match_algorithm import email_exact, email_domain, odoo_standard +from ..match_algorithm import email_domain, email_exact, odoo_standard MSG_BODY = [ - ('1 (RFC822 {1149}', - 'Return-Path: \r\n' - 'Delivered-To: demo@yourcompany.example.com\r\n' - 'Received: from localhost (localhost [127.0.0.1])\r\n' - '\tby vanaheim.acme.com (Postfix) with ESMTP id 14A3183163\r\n' - '\tfor ;' - ' Mon, 26 Mar 2018 16:03:52 +0200 (CEST)\r\n' - 'To: Test User \r\n' - 'From: Ronald Portier \r\n' - 'Subject: test\r\n' - 'Message-ID: <485a8041-d560-a981-5afc-d31c1f136748@acme.com>\r\n' - 'Date: Mon, 26 Mar 2018 16:03:51 +0200\r\n' - 'User-Agent: Mock Test\r\n' - 'MIME-Version: 1.0\r\n' - 'Content-Type: text/plain; charset=utf-8\r\n' - 'Content-Language: en-US\r\n' - 'Content-Transfer-Encoding: 7bit\r\n\r\n' - 'Hallo Wereld!\r\n'), - ')'] + ( + "1 (RFC822 {1149}", + "Return-Path: \r\n" + "Delivered-To: demo@yourcompany.example.com\r\n" + "Received: from localhost (localhost [127.0.0.1])\r\n" + "\tby vanaheim.acme.com (Postfix) with ESMTP id 14A3183163\r\n" + "\tfor ;" + " Mon, 26 Mar 2018 16:03:52 +0200 (CEST)\r\n" + "To: Test User \r\n" + "From: Ronald Portier \r\n" + "Subject: test\r\n" + "Message-ID: <485a8041-d560-a981-5afc-d31c1f136748@acme.com>\r\n" + "Date: Mon, 26 Mar 2018 16:03:51 +0200\r\n" + "User-Agent: Mock Test\r\n" + "MIME-Version: 1.0\r\n" + "Content-Type: text/plain; charset=utf-8\r\n" + "Content-Language: en-US\r\n" + "Content-Transfer-Encoding: 7bit\r\n\r\n" + "Hallo Wereld!\r\n", + ), + ")", +] -class MockConnection(): - +class MockConnection: def select(self, path): """Mock selecting a folder.""" - return ('OK', ) + return ("OK",) def store(self, msgid, msg_item, value): """Mock store command.""" - return 'OK' + return "OK" def fetch(self, msgid, parts): """Return RFC822 formatted message.""" - return ('OK', MSG_BODY) + return ("OK", MSG_BODY) def search(self, charset, criteria): """Return some msgid's.""" - return ('OK', ['123 456']) + return ("OK", ["123 456"]) class TestMatchAlgorithms(TransactionCase): - def _get_base_folder(self): - server_model = self.env['fetchmail.server'] - folder_model = self.env['fetchmail.server.folder'] + server_model = self.env["fetchmail.server"] + folder_model = self.env["fetchmail.server.folder"] folder = folder_model.browse([models.NewId()]) - folder.model_id = self.env.ref('base.model_res_partner').id - folder.model_field = 'email' - folder.match_algorithm = 'EmailExact' - folder.mail_field = 'to,from' + folder.model_id = self.env.ref("base.model_res_partner").id + folder.model_field = "email" + folder.match_algorithm = "EmailExact" + folder.mail_field = "to,from" folder.server_id = server_model.browse([models.NewId()]) return folder def do_matching( - self, match_algorithm, expected_xmlid, folder, mail_message, - mail_message_org=None): + self, + match_algorithm, + expected_xmlid, + folder, + mail_message, + mail_message_org=None, + ): matcher = match_algorithm() matches = matcher.search_matches(folder, mail_message) self.assertEqual(len(matches), 1) - self.assertEqual( - matches[0], self.env.ref(expected_xmlid)) + self.assertEqual(matches[0], self.env.ref(expected_xmlid)) connection = MockConnection() matcher.handle_match( - connection, matches[0], folder, mail_message, mail_message_org, - None) + connection, matches[0], folder, mail_message, mail_message_org, None + ) def test_email_exact(self): mail_message = { - 'subject': 'Testsubject', - 'to': 'demo@yourcompany.example.com', - 'from': 'someone@else.com', + "subject": "Testsubject", + "to": "demo@yourcompany.example.com", + "from": "someone@else.com", } folder = self._get_base_folder() - folder.match_algorithm = 'EmailExact' + folder.match_algorithm = "EmailExact" self.do_matching( - email_exact.EmailExact, 'base.user_demo_res_partner', - folder, mail_message) + email_exact.EmailExact, "base.user_demo_res_partner", folder, mail_message + ) self.assertEqual( - self.env.ref('base.user_demo_res_partner').message_ids.subject, - mail_message['subject']) + self.env.ref("base.user_demo_res_partner").message_ids.subject, + mail_message["subject"], + ) def test_email_domain(self): mail_message = { - 'subject': 'Testsubject', - 'to': 'test@seagate.com', - 'from': 'someone@else.com', - 'attachments': [('hello.txt', 'Hello World!')]} + "subject": "Testsubject", + "to": "test@seagate.com", + "from": "someone@else.com", + "attachments": [("hello.txt", "Hello World!")], + } folder = self._get_base_folder() - folder.match_algorithm = 'EmailDomain' + folder.match_algorithm = "EmailDomain" folder.use_first_match = True self.do_matching( - email_domain.EmailDomain, 'base.res_partner_address_31', - folder, mail_message) + email_domain.EmailDomain, + "base.res_partner_address_31", + folder, + mail_message, + ) self.assertEqual( - self.env.ref('base.res_partner_address_31').message_ids.subject, - mail_message['subject']) + self.env.ref("base.res_partner_address_31").message_ids.subject, + mail_message["subject"], + ) def test_odoo_standard(self): mail_message_org = ( @@ -113,20 +123,19 @@ class TestMatchAlgorithms(TransactionCase): "Hello world" ) folder = self._get_base_folder() - folder.match_algorithm = 'OdooStandard' + folder.match_algorithm = "OdooStandard" matcher = odoo_standard.OdooStandard() matches = matcher.search_matches(folder, None) self.assertEqual(len(matches), 1) - matcher.handle_match( - None, matches[0], folder, None, mail_message_org, None) + matcher.handle_match(None, matches[0], folder, None, mail_message_org, None) self.assertIn( - 'Hello world', - self.env['mail.message'] - .search([('subject', '=', 'testsubject')]).body) + "Hello world", + self.env["mail.message"].search([("subject", "=", "testsubject")]).body, + ) def test_apply_matching_exact(self): folder = self._get_base_folder() - folder.match_algorithm = 'EmailExact' + folder.match_algorithm = "EmailExact" connection = MockConnection() msgid = "<485a8041-d560-a981-5afc-d31c1f136748@acme.com>" matcher = email_exact.EmailExact() @@ -134,15 +143,15 @@ class TestMatchAlgorithms(TransactionCase): def test_retrieve_imap_folder_domain(self): folder = self._get_base_folder() - folder.match_algorithm = 'EmailDomain' + 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'] + 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']) + "match_algorithm", view["fields"]["folder_ids"]["views"]["form"]["arch"] + ) diff --git a/fetchmail_attach_from_folder/views/fetchmail_server.xml b/fetchmail_attach_from_folder/views/fetchmail_server.xml index 80620e263..cd5b9a095 100644 --- a/fetchmail_attach_from_folder/views/fetchmail_server.xml +++ b/fetchmail_attach_from_folder/views/fetchmail_server.xml @@ -1,4 +1,4 @@ - + @@ -8,21 +8,19 @@ {'required': [('type', '!=', 'imap')]} + name="attrs" + >{'required': [('type', '!=', 'imap')]} - + + attrs="{'invisible': [('type','!=','imap')]}" + > - + @@ -40,36 +38,40 @@ type="object" name="button_confirm_folder" string="Test & Confirm" - states="draft"/> + states="draft" + />