[MERGE] [FORWARD] Forward port of addons 7.0 until revision 9008
[odoo/odoo.git] / addons / mail / mail_alias.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Business Applications
5 #    Copyright (C) 2012 OpenERP S.A. (<http://openerp.com>).
6 #
7 #    This program is free software: you can redistribute it and/or modify
8 #    it under the terms of the GNU Affero General Public License as
9 #    published by the Free Software Foundation, either version 3 of the
10 #    License, or (at your option) any later version.
11 #
12 #    This program is distributed in the hope that it will be useful,
13 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
14 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 #    GNU Affero General Public License for more details.
16 #
17 #    You should have received a copy of the GNU Affero General Public License
18 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
19 #
20 ##############################################################################
21
22 import logging
23 import re
24 import unicodedata
25
26 from openerp.osv import fields, osv
27 from openerp.tools import ustr
28 from openerp.modules.registry import RegistryManager
29 from openerp import SUPERUSER_ID
30
31 _logger = logging.getLogger(__name__)
32
33 # Inspired by http://stackoverflow.com/questions/517923
34 def remove_accents(input_str):
35     """Suboptimal-but-better-than-nothing way to replace accented
36     latin letters by an ASCII equivalent. Will obviously change the
37     meaning of input_str and work only for some cases"""
38     input_str = ustr(input_str)
39     nkfd_form = unicodedata.normalize('NFKD', input_str)
40     return u''.join([c for c in nkfd_form if not unicodedata.combining(c)])
41
42 class mail_alias(osv.Model):
43     """A Mail Alias is a mapping of an email address with a given OpenERP Document
44        model. It is used by OpenERP's mail gateway when processing incoming emails
45        sent to the system. If the recipient address (To) of the message matches
46        a Mail Alias, the message will be either processed following the rules
47        of that alias. If the message is a reply it will be attached to the
48        existing discussion on the corresponding record, otherwise a new
49        record of the corresponding model will be created.
50        
51        This is meant to be used in combination with a catch-all email configuration
52        on the company's mail server, so that as soon as a new mail.alias is
53        created, it becomes immediately usable and OpenERP will accept email for it.
54      """
55     _name = 'mail.alias'
56     _description = "Email Aliases"
57     _rec_name = 'alias_name'
58     _order = 'alias_model_id, alias_name'
59
60     def _get_alias_domain(self, cr, uid, ids, name, args, context=None):
61         ir_config_parameter = self.pool.get("ir.config_parameter")
62         domain = ir_config_parameter.get_param(cr, uid, "mail.catchall.domain", context=context)
63         return dict.fromkeys(ids, domain or "")
64
65     _columns = {
66         'alias_name': fields.char('Alias', required=True,
67                             help="The name of the email alias, e.g. 'jobs' "
68                                  "if you want to catch emails for <jobs@example.my.openerp.com>",),
69         'alias_model_id': fields.many2one('ir.model', 'Aliased Model', required=True, ondelete="cascade",
70                                           help="The model (OpenERP Document Kind) to which this alias "
71                                                "corresponds. Any incoming email that does not reply to an "
72                                                "existing record will cause the creation of a new record "
73                                                "of this model (e.g. a Project Task)",
74                                           # hack to only allow selecting mail_thread models (we might
75                                           # (have a few false positives, though)
76                                           domain="[('field_id.name', '=', 'message_ids')]"),
77         'alias_user_id': fields.many2one('res.users', 'Owner',
78                                            help="The owner of records created upon receiving emails on this alias. "
79                                                 "If this field is not set the system will attempt to find the right owner "
80                                                 "based on the sender (From) address, or will use the Administrator account "
81                                                 "if no system user is found for that address."),
82         'alias_defaults': fields.text('Default Values', required=True,
83                                       help="A Python dictionary that will be evaluated to provide "
84                                            "default values when creating new records for this alias."),
85         'alias_force_thread_id': fields.integer('Record Thread ID',
86                                       help="Optional ID of a thread (record) to which all incoming "
87                                            "messages will be attached, even if they did not reply to it. "
88                                            "If set, this will disable the creation of new records completely."),
89         'alias_domain': fields.function(_get_alias_domain, string="Alias domain", type='char', size=None),
90     }
91
92     _defaults = {
93         'alias_defaults': '{}',
94         'alias_user_id': lambda self,cr,uid,context: uid,
95         # looks better when creating new aliases - even if the field is informative only
96         'alias_domain': lambda self,cr,uid,context: self._get_alias_domain(cr, SUPERUSER_ID,[1],None,None)[1]
97     }
98
99     _sql_constraints = [
100         ('alias_unique', 'UNIQUE(alias_name)', 'Unfortunately this email alias is already used, please choose a unique one')
101     ]
102
103     def _check_alias_defaults(self, cr, uid, ids, context=None):
104         try:
105             for record in self.browse(cr, uid, ids, context=context):
106                 dict(eval(record.alias_defaults))
107         except Exception:
108             return False
109         return True
110
111     _constraints = [
112         (_check_alias_defaults, '''Invalid expression, it must be a literal python dictionary definition e.g. "{'field': 'value'}"''', ['alias_defaults']),
113     ]
114
115     def name_get(self, cr, uid, ids, context=None):
116         """Return the mail alias display alias_name, inclusing the implicit
117            mail catchall domain from config.
118            e.g. `jobs@openerp.my.openerp.com` or `sales@openerp.my.openerp.com`
119         """
120         res = []
121         for record in self.browse(cr, uid, ids, context=context):
122             if record.alias_name and record.alias_domain:
123                 res.append((record['id'], "%s@%s" % (record.alias_name, record.alias_domain)))
124             else:
125                 res.append((record['id'], False))
126         return res
127
128     def _find_unique(self, cr, uid, name, context=None):
129         """Find a unique alias name similar to ``name``. If ``name`` is
130            already taken, make a variant by adding an integer suffix until
131            an unused alias is found.
132         """
133         sequence = None
134         while True:
135             new_name = "%s%s" % (name, sequence) if sequence is not None else name
136             if not self.search(cr, uid, [('alias_name', '=', new_name)]):
137                 break
138             sequence = (sequence + 1) if sequence else 2
139         return new_name
140
141     def migrate_to_alias(self, cr, child_model_name, child_table_name, child_model_auto_init_fct,
142         alias_id_column, alias_key, alias_prefix = '', alias_force_key = '', alias_defaults = {}, context=None):
143         """ Installation hook to create aliases for all users and avoid constraint errors.
144
145             :param child_model_name: model name of the child class (i.e. res.users)
146             :param child_table_name: table name of the child class (i.e. res_users)
147             :param child_model_auto_init_fct: pointer to the _auto_init function
148                 (i.e. super(res_users,self)._auto_init(cr, context=context))
149             :param alias_id_column: alias_id column (i.e. self._columns['alias_id'])
150             :param alias_key: name of the column used for the unique name (i.e. 'login')
151             :param alias_prefix: prefix for the unique name (i.e. 'jobs' + ...)
152             :param alias_force_key': name of the column for force_thread_id;
153                 if empty string, not taken into account
154             :param alias_defaults: dict, keys = mail.alias columns, values = child
155                 model column name used for default values (i.e. {'job_id': 'id'})
156         """
157
158         # disable the unique alias_id not null constraint, to avoid spurious warning during 
159         # super.auto_init. We'll reinstall it afterwards.
160         alias_id_column.required = False
161
162         # call _auto_init
163         child_model_auto_init_fct(cr, context=context)
164
165         registry = RegistryManager.get(cr.dbname)
166         mail_alias = registry.get('mail.alias')
167         child_class_model = registry[child_model_name]
168         no_alias_ids = child_class_model.search(cr, SUPERUSER_ID, [('alias_id', '=', False)], context={'active_test':False})
169         # Use read() not browse(), to avoid prefetching uninitialized inherited fields
170         for obj_data in child_class_model.read(cr, SUPERUSER_ID, no_alias_ids, [alias_key]):
171             alias_vals = {'alias_name': '%s%s' % (alias_prefix, obj_data[alias_key]) }
172             if alias_force_key:
173                 alias_vals['alias_force_thread_id'] = obj_data[alias_force_key]
174             alias_vals['alias_defaults'] = dict( (k, obj_data[v]) for k, v in alias_defaults.iteritems())
175             alias_id = mail_alias.create_unique_alias(cr, SUPERUSER_ID, alias_vals, model_name=child_model_name)
176             child_class_model.write(cr, SUPERUSER_ID, obj_data['id'], {'alias_id': alias_id})
177             _logger.info('Mail alias created for %s %s (uid %s)', child_model_name, obj_data[alias_key], obj_data['id'])
178
179         # Finally attempt to reinstate the missing constraint
180         try:
181             cr.execute('ALTER TABLE %s ALTER COLUMN alias_id SET NOT NULL' % (child_table_name))
182         except Exception:
183             _logger.warning("Table '%s': unable to set a NOT NULL constraint on column '%s' !\n"\
184                             "If you want to have it, you should update the records and execute manually:\n"\
185                             "ALTER TABLE %s ALTER COLUMN %s SET NOT NULL",
186                             child_table_name, 'alias_id', child_table_name, 'alias_id')
187
188         # set back the unique alias_id constraint
189         alias_id_column.required = True
190
191     def create_unique_alias(self, cr, uid, vals, model_name=None, context=None):
192         """Creates an email.alias record according to the values provided in ``vals``,
193         with 2 alterations: the ``alias_name`` value may be suffixed in order to
194         make it unique (and certain unsafe characters replaced), and 
195         he ``alias_model_id`` value will set to the model ID of the ``model_name``
196         value, if provided, 
197         """
198         # when an alias name appears to already be an email, we keep the local part only
199         alias_name = remove_accents(vals['alias_name']).lower().split('@')[0]
200         alias_name = re.sub(r'[^\w+.]+', '-', alias_name)
201         alias_name = self._find_unique(cr, uid, alias_name, context=context)
202         vals['alias_name'] = alias_name
203         if model_name:
204             model_id = self.pool.get('ir.model').search(cr, uid, [('model', '=', model_name)], context=context)[0]
205             vals['alias_model_id'] = model_id
206         return self.create(cr, uid, vals, context=context)