diff --git a/fetchmail_attach_from_folder/__init__.py b/fetchmail_attach_from_folder/__init__.py new file mode 100644 index 000000000..1c91fe478 --- /dev/null +++ b/fetchmail_attach_from_folder/__init__.py @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import match_algorithm +import model +import wizard diff --git a/fetchmail_attach_from_folder/__openerp__.py b/fetchmail_attach_from_folder/__openerp__.py new file mode 100644 index 000000000..6b0d58087 --- /dev/null +++ b/fetchmail_attach_from_folder/__openerp__.py @@ -0,0 +1,45 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +{ + 'name': 'Attach mails in an IMAP folder to existing objects', + 'version': '1.0', + 'description': """ + Adds the possibility to attach emails from a certain IMAP folder to objects, + ie partners. Matching is done via several algorithms, ie email address. + + This gives a simple possibility to archive emails in OpenERP without a mail + client integration. + """, + 'author': 'Therp BV', + 'website': 'http://www.therp.nl', + "category": "Tools", + "depends": ['fetchmail'], + 'data': [ + 'view/fetchmail_server.xml', + 'wizard/attach_mail_manually.xml', + ], + 'js': [], + 'installable': True, + 'active': False, + 'certificate': '', +} diff --git a/fetchmail_attach_from_folder/match_algorithm/__init__.py b/fetchmail_attach_from_folder/match_algorithm/__init__.py new file mode 100644 index 000000000..ff3610863 --- /dev/null +++ b/fetchmail_attach_from_folder/match_algorithm/__init__.py @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import base +import email_exact +import email_domain +import openerp_standard diff --git a/fetchmail_attach_from_folder/match_algorithm/base.py b/fetchmail_attach_from_folder/match_algorithm/base.py new file mode 100644 index 000000000..5116c929a --- /dev/null +++ b/fetchmail_attach_from_folder/match_algorithm/base.py @@ -0,0 +1,43 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +class base(object): + name = None + '''Name shown to the user''' + + required_fields = [] + '''Fields on fetchmail_server folder that are required for this algorithm''' + + readonly_fields = [] + '''Fields on fetchmail_server folder that are readonly for this algorithm''' + + + def search_matches(self, cr, uid, conf, mail_message, mail_message_org): + '''Returns ids found for model with mail_message''' + return [] + + def handle_match( + self, cr, uid, connection, object_id, folder, + mail_message, mail_message_org, msgid, context=None): + '''Do whatever it takes to handle a match''' + return folder.server_id.attach_mail(connection, object_id, folder, + mail_message, msgid) diff --git a/fetchmail_attach_from_folder/match_algorithm/email_domain.py b/fetchmail_attach_from_folder/match_algorithm/email_domain.py new file mode 100644 index 000000000..ad86faacd --- /dev/null +++ b/fetchmail_attach_from_folder/match_algorithm/email_domain.py @@ -0,0 +1,44 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from email_exact import email_exact + +class email_domain(email_exact): + '''Search objects by domain name of email address. + Beware of match_first here, this is most likely to ge it wrong (gmail...)''' + name = 'Domain of email address' + + def search_matches(self, cr, uid, conf, mail_message, mail_message_org): + ids = super(email_domain, self).search_matches( + cr, uid, conf, mail_message, mail_message_org) + if not ids: + domains = [] + for addr in self._get_mailaddresses(conf, mail_message): + domains.append(addr.split('@')[-1]) + ids = conf.pool.get(conf.model_id.model).search( + cr, uid, + self._get_mailaddress_search_domain( + conf, mail_message, + operator='like', + values=['%@'+domain for domain in set(domains)]), + order=conf.model_order) + return ids diff --git a/fetchmail_attach_from_folder/match_algorithm/email_exact.py b/fetchmail_attach_from_folder/match_algorithm/email_exact.py new file mode 100644 index 000000000..0e67e7222 --- /dev/null +++ b/fetchmail_attach_from_folder/match_algorithm/email_exact.py @@ -0,0 +1,52 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from base import base +from openerp.tools.safe_eval import safe_eval +from openerp.addons.mail.mail_message import to_email + +class email_exact(base): + name = 'Exact mailadress' + required_fields = ['model_field', 'mail_field'] + + def _get_mailaddresses(self, conf, mail_message): + mailaddresses = [] + fields = conf.mail_field.split(',') + for field in fields: + mailaddresses+=to_email(mail_message[field]) + return mailaddresses + + def _get_mailaddress_search_domain( + self, conf, mail_message, operator='=', values=None): + mailaddresses = values or self._get_mailaddresses( + conf, mail_message) + if not mailaddresses: + return [(0,'=',1)] + return ((['|'] * (len(mailaddresses) - 1)) + [ + (conf.model_field, operator, addr) for addr in mailaddresses] + + safe_eval(conf.domain or '[]')) + + def search_matches(self, cr, uid, conf, mail_message, mail_message_org): + return conf.pool.get(conf.model_id.model).search( + cr, uid, + self._get_mailaddress_search_domain(conf, mail_message), + order=conf.model_order) diff --git a/fetchmail_attach_from_folder/match_algorithm/openerp_standard.py b/fetchmail_attach_from_folder/match_algorithm/openerp_standard.py new file mode 100644 index 000000000..ae8662ef2 --- /dev/null +++ b/fetchmail_attach_from_folder/match_algorithm/openerp_standard.py @@ -0,0 +1,48 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from base import base +from openerp.tools.safe_eval import safe_eval + +class openerp_standard(base): + name = 'OpenERP standard' + readonly_fields = ['model_field', 'mail_field', 'match_first', 'domain', + 'model_order', 'flag_nonmatching'] + + def search_matches(self, cr, uid, conf, mail_message, mail_message_org): + '''Always match. Duplicates will be fished out by message_id''' + return [True] + + def handle_match( + self, cr, uid, connection, object_id, folder, + mail_message, mail_message_org, msgid, context): + result = folder.pool.get('mail.thread').message_process( + cr, uid, + folder.model_id.model, mail_message_org, + save_original=folder.server_id.original, + strip_attachments=(not folder.server_id.attach), + context=context) + + if folder.delete_matching: + connection.store(msgid, '+FLAGS', '\\DELETED') + + return result diff --git a/fetchmail_attach_from_folder/model/__init__.py b/fetchmail_attach_from_folder/model/__init__.py new file mode 100644 index 000000000..d7e030949 --- /dev/null +++ b/fetchmail_attach_from_folder/model/__init__.py @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import fetchmail_server +import fetchmail_server_folder diff --git a/fetchmail_attach_from_folder/model/fetchmail_server.py b/fetchmail_attach_from_folder/model/fetchmail_server.py new file mode 100644 index 000000000..e92bde8e9 --- /dev/null +++ b/fetchmail_attach_from_folder/model/fetchmail_server.py @@ -0,0 +1,224 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +import simplejson +from lxml import etree +from openerp.osv.orm import Model, except_orm, browse_null +from openerp.tools.translate import _ +from openerp.osv import fields +from openerp.addons.fetchmail.fetchmail import logger +from openerp.tools.misc import UnquoteEvalContext +from openerp.tools.safe_eval import safe_eval + +class fetchmail_server(Model): + _inherit = 'fetchmail.server' + + _columns = { + 'folder_ids': fields.one2many( + 'fetchmail.server.folder', 'server_id', 'Folders'), + } + + _defaults = { + 'type': 'imap', + } + + def __init__(self, pool, cr): + self._columns['object_id'].required=False + return super(fetchmail_server, self).__init__(pool, cr) + + def onchange_server_type( + self, cr, uid, ids, server_type=False, ssl=False, + object_id=False): + retval = super( + fetchmail_server, self).onchange_server_type(cr, uid, + ids, server_type, ssl, object_id) + retval['value']['state']='draft' + return retval + + def fetch_mail(self, cr, uid, ids, context=None): + if context is None: + context = {} + + check_original = [] + + for this in self.browse(cr, uid, ids, context): + if this.object_id: + check_original.append(this.id) + + context.update( + { + 'fetchmail_server_id': this.id, + 'server_type': this.type + }) + + connection = this.connect() + for folder in this.folder_ids: + logger.info('start checking for emails in %s server %s', + folder.path, this.name) + matcher = folder.get_algorithm() + + if connection.select(folder.path)[0] != 'OK': + logger.error( + 'Could not open mailbox %s on %s' % ( + folder.path, this.server)) + connection.select() + continue + result, msgids = connection.search(None, 'UNDELETED') + if result != 'OK': + logger.error( + 'Could not search mailbox %s on %s' % ( + folder.path, this.server)) + continue + for msgid in msgids[0].split(): + result, msgdata = connection.fetch(msgid, '(RFC822)') + if result != 'OK': + logger.error( + 'Could not fetch %s in %s on %s' % ( + msgid, folder.path, this.server)) + continue + + mail_message = self.pool.get('mail.message').parse_message( + msgdata[0][1], this.original) + + if self.pool.get('mail.message').search(cr, uid, [ + ('message_id','=',mail_message['message-id'])]): + continue + + found_ids = matcher.search_matches( + cr, uid, folder, + mail_message, msgdata[0][1]) + + if found_ids and (len(found_ids) == 1 or + folder.match_first): + try: + matcher.handle_match( + cr, uid, connection, + found_ids[0], folder, mail_message, + msgdata[0][1], msgid, context) + cr.commit() + except Exception, e: + cr.rollback() + logger.exception( + "Failed to fetch mail %s from %s", + msgid, this.name) + elif folder.flag_nonmatching: + connection.store(msgid, '+FLAGS', '\\FLAGGED') + connection.close() + + return super(fetchmail_server, self).fetch_mail( + cr, uid, check_original, context) + + def attach_mail( + self, cr, uid, ids, connection, object_id, folder, + mail_message, msgid, context=None): + for this in self.browse(cr, uid, ids, context): + partner_id = None + if folder.model_id.model == 'res.partner': + partner_id = object_id + if self.pool.get(folder.model_id.model)._columns.\ + has_key('partner_id'): + partner_id=self.pool.get( + folder.model_id.model).browse( + cr, uid, object_id, context + ).partner_id.id + + self.pool.get('mail.message').create( + cr, uid, + { + 'partner_id': partner_id, + 'model': folder.model_id.model, + 'res_id': object_id, + 'body_text': mail_message.get('body'), + 'body_html': mail_message.get('body_html'), + 'subject': mail_message.get('subject'), + 'email_to': mail_message.get('to'), + 'email_from': mail_message.get('from'), + 'email_cc': mail_message.get('cc'), + 'reply_to': mail_message.get('reply'), + 'date': mail_message.get('date'), + 'message_id': mail_message.get('message-id'), + 'subtype': mail_message.get('subtype'), + 'headers': mail_message.get('headers'), + }, + context) + if this.attach: + #TODO: create attachments + pass + if folder.delete_matching: + connection.store(msgid, '+FLAGS', '\\DELETED') + + def button_confirm_login(self, cr, uid, ids, context=None): + retval = super(fetchmail_server, self).button_confirm_login(cr, uid, + ids, context) + + for this in self.browse(cr, uid, ids, context): + this.write({'state': 'draft'}) + connection = this.connect() + connection.select() + for folder in this.folder_ids: + if connection.select(folder.path)[0] != 'OK': + raise except_orm( + _('Error'), _('Mailbox %s not found!') % + folder.path) + folder.get_algorithm().search_matches( + cr, uid, folder, browse_null(), '') + connection.close() + this.write({'state': 'done'}) + + return retval + + def fields_view_get(self, cr, user, view_id=None, view_type='form', + context=None, toolbar=False, submenu=False): + result = super(fetchmail_server, self).fields_view_get( + cr, user, view_id, view_type, context, toolbar, submenu) + + if view_type == 'form': + view = etree.fromstring( + result['fields']['folder_ids']['views']['tree']['arch']) + modifiers={} + docstr='' + for algorithm in self.pool.get('fetchmail.server.folder')\ + ._get_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.__doc__) or '' + + for field in view: + if field.tag == 'field' and field.get('name') in modifiers: + field.set('modifiers', simplejson.dumps( + dict( + eval(field.attrib['modifiers'], + UnquoteEvalContext({})), + **modifiers[field.attrib['name']]))) + if (field.tag == 'field' and + field.get('name') == 'match_algorithm'): + field.set('help', docstr) + result['fields']['folder_ids']['views']['tree']['arch'] = \ + etree.tostring(view) + + return result diff --git a/fetchmail_attach_from_folder/model/fetchmail_server_folder.py b/fetchmail_attach_from_folder/model/fetchmail_server_folder.py new file mode 100644 index 000000000..6515b5528 --- /dev/null +++ b/fetchmail_attach_from_folder/model/fetchmail_server_folder.py @@ -0,0 +1,86 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# OpenERP, Open Source Management Solution +# This module copyright (C) 2013 Therp BV () +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +############################################################################## + +from openerp.osv import fields +from openerp.osv.orm import Model +from .. import match_algorithm + +class fetchmail_server_folder(Model): + _name = 'fetchmail.server.folder' + _rec_name = 'path' + + 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)]) + + def _get_match_algorithms_sel(self, cr, uid, context=None): + algorithms=[] + for cls in self._get_match_algorithms().itervalues(): + algorithms.append((cls.__name__, cls.name)) + return tuple(sorted(algorithms, lambda a, b: cmp(a[0], b[0]))) + + _columns = { + 'sequence': fields.integer('Sequence'), + 'path': fields.char( + 'Path', size=256, help='The path to your mail ' + 'folder. Typically would be something like \'INBOX.myfolder\'', + required=True), + 'model_id': fields.many2one('ir.model', 'Model', required=True), + 'model_field': fields.char('Field (model)', size=128), + 'model_order': fields.char('Order (model)', size=128), + 'match_algorithm': fields.selection( + _get_match_algorithms_sel, + 'Match algorithm', required=True, translate=True), + 'mail_field': fields.char('Field (email)', size=128), + 'server_id': fields.many2one('fetchmail.server', 'Server'), + 'delete_matching': fields.boolean('Delete matches'), + 'flag_nonmatching': fields.boolean('Flag nonmatching'), + 'match_first': fields.boolean('Use 1st match'), + 'domain': fields.char( + 'Domain', size=128, help='Fill in a search ' + 'filter to narrow down objects to match') + } + + _defaults = { + 'flag_nonmatching': True, + } + + def get_algorithm(self, cr, uid, ids, context=None): + for this in self.browse(cr, uid, ids, context): + return self._get_match_algorithms()[this.match_algorithm]() + + def button_attach_mail_manually(self, cr, uid, ids, context=None): + for this in self.browse(cr, uid, ids, context): + context.update({'default_folder_id': this.id}) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'fetchmail.attach.mail.manually', + 'target': 'new', + 'context': context, + 'view_type': 'form', + 'view_mode': 'form', + } + diff --git a/fetchmail_attach_from_folder/view/fetchmail_server.xml b/fetchmail_attach_from_folder/view/fetchmail_server.xml new file mode 100644 index 000000000..172166d45 --- /dev/null +++ b/fetchmail_attach_from_folder/view/fetchmail_server.xml @@ -0,0 +1,48 @@ + + + + + fetchmail.server.form + fetchmail.server + form + + + + + {'required': [('type', '!=', 'imap')]} + + + + + + + + + + + + + + + + + +