def _create_moderation_rule(self, status): """Create a moderation rule <mail.group.moderation> with the given status. Update existing moderation rule for the same email address if found, otherwise create a new rule. """ if status not in ('ban', 'allow'): raise ValueError(_('Wrong status (%s)', status)) for message in self: if not email_normalize(message.email_from): raise UserError( _('The email "%s" is not valid.', message.email_from)) existing_moderation = self.env['mail.group.moderation'].search( expression.OR([[('email', '=', email_normalize(message.email_from)), ('mail_group_id', '=', message.mail_group_id.id)] for message in self])) existing_moderation.status = status # Add the value in a set to create only 1 moderation rule per (email_normalized, group) moderation_to_create = { (email_normalize(message.email_from), message.mail_group_id.id) for message in self if email_normalize( message.email_from) not in existing_moderation.mapped('email') } self.env['mail.group.moderation'].create([{ 'email': email, 'mail_group_id': mail_group_id, 'status': status, } for email, mail_group_id in moderation_to_create])
def _odoo_attendee_commands(self, google_event): attendee_commands = [] partner_commands = [] google_attendees = google_event.attendees or [] if len(google_attendees) == 0 and google_event.organizer and google_event.organizer.get('self', False): user = google_event.owner(self.env) google_attendees += [{ 'email': user.partner_id.email, 'status': {'response': 'accepted'}, }] emails = [a.get('email') for a in google_attendees] existing_attendees = self.env['calendar.attendee'] if google_event.exists(self.env): existing_attendees = self.browse(google_event.odoo_id(self.env)).attendee_ids attendees_by_emails = {tools.email_normalize(a.email): a for a in existing_attendees} for attendee in google_attendees: email = attendee.get('email') if email in attendees_by_emails: # Update existing attendees attendee_commands += [(1, attendees_by_emails[email].id, {'state': attendee.get('responseStatus')})] else: # Create new attendees partner = self.env.user.partner_id if attendee.get('self') else self.env['res.partner'].find_or_create(attendee.get('email')) attendee_commands += [(0, 0, {'state': attendee.get('responseStatus'), 'partner_id': partner.id})] partner_commands += [(4, partner.id)] if attendee.get('displayName') and not partner.name: partner.name = attendee.get('displayName') for odoo_attendee in attendees_by_emails.values(): # Remove old attendees if tools.email_normalize(odoo_attendee.email) not in emails: attendee_commands += [(2, odoo_attendee.id)] partner_commands += [(3, odoo_attendee.partner_id.id)] return attendee_commands, partner_commands
def event_track_proposal_post(self, event, **post): if not event.can_access_from_current_website(): raise NotFound() # Only accept existing tag indices. Use search instead of browse + exists: # this prevents users to register colorless tags if not allowed to (ACL). input_tag_indices = [int(tag_id) for tag_id in post['tags'].split(',') if tag_id] valid_tag_indices = request.env['event.track.tag'].search([('id', 'in', input_tag_indices)]).ids contact = request.env['res.partner'] visitor_partner = request.env['website.visitor']._get_visitor_from_request().partner_id # Contact name is required. Therefore, empty contacts are not considered here. At least one of contact_phone # and contact_email must be filled. Email is verified. If the post tries to create contact with no valid entry, # raise exception. If normalized email is the same as logged partner, use its partner_id on track instead. # This prevents contact duplication. Otherwise, create new contact with contact additional info of post. if post.get('add_contact_information'): valid_contact_email = tools.email_normalize(post.get('contact_email')) # Here, the phone is not formatted. To format it, one needs a country. Based on a country, from geoip for instance. # The problem is that one could propose a track in country A with phone number of country B. Validity is therefore # quite tricky. We accept any format of contact_phone. Could be improved with select country phone widget. if valid_contact_email or post.get('contact_phone'): if visitor_partner and valid_contact_email == visitor_partner.email_normalized: contact = visitor_partner else: contact = request.env['res.partner'].sudo().create({ 'email': valid_contact_email, 'name': post.get('contact_name'), 'phone': post.get('contact_phone'), }) else: raise exceptions.ValidationError(_("Format Error : please enter a valid contact phone or contact email.")) # If the speaker email is the same as logged user's, then also uses its partner on track, same as above. else: valid_speaker_email = tools.email_normalize(post['partner_email']) if visitor_partner and valid_speaker_email == visitor_partner.email_normalized: contact = visitor_partner track = request.env['event.track'].with_context({'mail_create_nosubscribe': True}).sudo().create({ 'name': post['track_name'], 'partner_id': contact.id, 'partner_name': post['partner_name'], 'partner_email': post['partner_email'], 'partner_phone': post['partner_phone'], 'partner_function': post['partner_function'], 'contact_phone': contact.phone, 'contact_email': contact.email, 'event_id': event.id, 'tag_ids': [(6, 0, valid_tag_indices)], 'description': plaintext2html(post['description']), 'partner_biography': plaintext2html(post['partner_biography']), 'user_id': False, 'image': base64.b64encode(post['image'].read()) if post.get('image') else False, }) if request.env.user != request.website.user_id: track.sudo().message_subscribe(partner_ids=request.env.user.partner_id.ids) return request.redirect('/event/%s/track_proposal/success/%s' % (event.id, track.id))
def _mail_cc_sanitized_raw_dict(self, cc_string): '''return a dict of sanitize_email:raw_email from a string of cc''' if not cc_string: return {} return { tools.email_normalize(email): formataddr( (name, tools.email_normalize(email))) for (name, email) in tools.email_split_tuples(cc_string) }
def _find_mail_server(self, email_from, mail_servers=None): """Find the appropriate mail server for the given email address. Returns: Record<ir.mail_server>, email_from - Mail server to use to send the email (None if we use the odoo-bin arguments) - Email FROM to use to send the email (in some case, it might be impossible to use the given email address directly if no mail server is configured for) """ email_from_normalized = email_normalize(email_from) email_from_domain = email_domain_extract(email_from_normalized) notifications_email = email_normalize(self._get_default_from_address()) notifications_domain = email_domain_extract(notifications_email) if mail_servers is None: mail_servers = self.sudo().search([], order='sequence') # 1. Try to find a mail server for the right mail from mail_server = mail_servers.filtered(lambda m: email_normalize(m.from_filter) == email_from_normalized) if mail_server: return mail_server[0], email_from mail_server = mail_servers.filtered(lambda m: email_domain_normalize(m.from_filter) == email_from_domain) if mail_server: return mail_server[0], email_from # 2. Try to find a mail server for <*****@*****.**> if notifications_email: mail_server = mail_servers.filtered(lambda m: email_normalize(m.from_filter) == notifications_email) if mail_server: return mail_server[0], notifications_email mail_server = mail_servers.filtered(lambda m: email_domain_normalize(m.from_filter) == notifications_domain) if mail_server: return mail_server[0], notifications_email # 3. Take the first mail server without "from_filter" because # nothing else has been found... Will spoof the FROM because # we have no other choices mail_server = mail_servers.filtered(lambda m: not m.from_filter) if mail_server: return mail_server[0], email_from # 4. Return the first mail server even if it was configured for another domain if mail_servers: return mail_servers[0], email_from # 5: SMTP config in odoo-bin arguments from_filter = self.env['ir.config_parameter'].sudo().get_param( 'mail.default.from_filter', tools.config.get('from_filter')) if self._match_from_filter(email_from, from_filter): return None, email_from if notifications_email and self._match_from_filter(notifications_email, from_filter): return None, notifications_email return None, email_from
def _odoo_attendee_commands(self, google_event): attendee_commands = [] partner_commands = [] google_attendees = google_event.attendees or [] if len( google_attendees ) == 0 and google_event.organizer and google_event.organizer.get( 'self', False): user = google_event.owner(self.env) google_attendees += [{ 'email': user.partner_id.email, 'responseStatus': 'needsAction', }] emails = [a.get('email') for a in google_attendees] existing_attendees = self.env['calendar.attendee'] if google_event.exists(self.env): existing_attendees = self.browse(google_event.odoo_id( self.env)).attendee_ids attendees_by_emails = { tools.email_normalize(a.email): a for a in existing_attendees } partners = self._get_sync_partner(emails) for attendee in zip(emails, partners, google_attendees): email = attendee[0] if email in attendees_by_emails: # Update existing attendees attendee_commands += [(1, attendees_by_emails[email].id, { 'state': attendee[2].get('responseStatus') })] else: # Create new attendees if attendee[2].get('self'): partner = self.env.user.partner_id elif attendee[1]: partner = attendee[1] else: continue attendee_commands += [(0, 0, { 'state': attendee[2].get('responseStatus'), 'partner_id': partner.id })] partner_commands += [(4, partner.id)] if attendee[2].get('displayName') and not partner.name: partner.name = attendee[2].get('displayName') for odoo_attendee in attendees_by_emails.values(): # Remove old attendees but only if it does not correspond to the current user. email = tools.email_normalize(odoo_attendee.email) if email not in emails and email != self.env.user.email: attendee_commands += [(2, odoo_attendee.id)] partner_commands += [(3, odoo_attendee.partner_id.id)] return attendee_commands, partner_commands
def _match_from_filter(self, email_from, from_filter): """Return True is the given email address match the "from_filter" field. The from filter can be Falsy (always match), a domain name or an full email address. """ if not from_filter: return True normalized_mail_from = email_normalize(email_from) if '@' in from_filter: return email_normalize(from_filter) == normalized_mail_from return email_domain_extract(normalized_mail_from) == email_domain_normalize(from_filter)
def _create_portal_users(self): partners_without_user = self.filtered(lambda partner: not partner.user_ids) if not partners_without_user: return self.env['res.users'] created_users = self.env['res.users'] for partner in partners_without_user: created_users += self.env['res.users'].with_context(no_reset_password=True).sudo()._create_user_from_template({ 'email': email_normalize(partner.email), 'login': email_normalize(partner.email), 'partner_id': partner.id, 'company_id': self.env.company.id, 'company_ids': [(6, 0, self.env.company.ids)], 'active': True, }) return created_users
def insert_record(self, request, model, values, custom, meta=None): is_lead_model = model.model == 'crm.lead' if is_lead_model: values_email_normalized = tools.email_normalize(values.get('email_from')) visitor_sudo = request.env['website.visitor']._get_visitor_from_request() visitor_partner = visitor_sudo.partner_id if values_email_normalized and visitor_partner and visitor_partner.email_normalized == values_email_normalized: # Here, 'phone' in values has already been formatted, see _handle_website_form. values_phone = values.get('phone') # We write partner id on crm only if no phone exists on partner or in input, # or if both numbers (after formating) are the same. This way we get additional phone # if possible, without modifying an existing one. (see inverse function on model crm.lead) if values_phone and visitor_partner.phone: if visitor_partner._phone_format(visitor_partner.phone) == values_phone: values['partner_id'] = visitor_partner.id else: values['partner_id'] = visitor_partner.id if 'company_id' not in values: values['company_id'] = request.website.company_id.id lang = request.context.get('lang', False) values['lang_id'] = values.get('lang_id') or request.env['res.lang']._lang_get_id(lang) result = super(WebsiteForm, self).insert_record(request, model, values, custom, meta=meta) if is_lead_model and visitor_sudo and result: lead_sudo = request.env['crm.lead'].browse(result).sudo() if lead_sudo.exists(): vals = {'lead_ids': [(4, result)]} if not visitor_sudo.lead_ids and not visitor_sudo.partner_id: vals['name'] = lead_sudo.contact_name visitor_sudo.write(vals) return result
def res_partners_search(self, search_term, limit=30, **kwargs): """ Used for the plugin search contact functionality where the user types a string query in order to search for matching contacts, the string query can either be the name of the contact, it's reference or it's email. We choose these fields because these are probably the most interesting fields that the user can perform a search on. The method returns an array containing the dicts of the matched contacts. """ normalized_email = tools.email_normalize(search_term) if normalized_email: filter_domain = [('email_normalized', '=', search_term)] else: filter_domain = ['|', '|', ('display_name', 'ilike', search_term), ('ref', '=', search_term), ('email', 'ilike', search_term)] # Search for the partner based on the email. # If multiple are found, take the first one. partners = request.env['res.partner'].search(filter_domain, limit=limit) partners = [ self._get_partner_data(partner) for partner in partners ] return {"partners": partners}
def action_invite(self): """ Process the wizard content and proceed with sending the related email(s), rendering any template patterns on the fly if needed """ self.ensure_one() Partner = self.env['res.partner'] # compute partners and emails, try to find partners for given emails valid_partners = self.partner_ids langs = set(valid_partners.mapped('lang')) - {False} if len(langs) == 1: self = self.with_context(lang=langs.pop()) valid_emails = [] for email in emails_split.split(self.emails or ''): partner = False email_normalized = tools.email_normalize(email) if email_normalized: limit = None if self.survey_users_login_required else 1 partner = Partner.search( [('email_normalized', '=', email_normalized)], limit=limit) if partner: valid_partners |= partner else: email_formatted = tools.email_split_and_format(email) if email_formatted: valid_emails.extend(email_formatted) if not valid_partners and not valid_emails: raise UserError(_("Please enter at least one valid recipient.")) answers = self._prepare_answers(valid_partners, valid_emails) for answer in answers: self._send_mail(answer) return {'type': 'ir.actions.act_window_close'}
def _process_answer(self, mail_channel, message_body): """ Method called when the user reacts to the current chatbot.script step. For most chatbot.script.step#step_types it simply returns the next chatbot.script.step of the script (see '_fetch_next_step'). Some extra processing is done for steps of type 'question_email' and 'question_phone' where we store the user raw answer (the mail message HTML body) into the chatbot.message in order to be able to recover it later (see '_chatbot_prepare_customer_values'). :param mail_channel: :param message_body: :return: script step to display next :rtype: 'chatbot.script.step' """ self.ensure_one() user_text_answer = html2plaintext(message_body) if self.step_type == 'question_email' and not email_normalize(user_text_answer): # if this error is raised, display an error message but do not go to next step raise ValidationError(_('"%s" is not a valid email.', user_text_answer)) if self.step_type in ['question_email', 'question_phone']: chatbot_message = self.env['chatbot.message'].search([ ('mail_channel_id', '=', mail_channel.id), ('script_step_id', '=', self.id), ], limit=1) if chatbot_message: chatbot_message.write({'user_raw_answer': message_body}) chatbot_message.flush() return self._fetch_next_step(mail_channel.chatbot_message_ids.user_script_answer_id)
def iap_enrich(self, from_cron=False): lead_emails = {} for lead in self: # If lead is lost, active == False, but is anyway removed from the search in the cron. if lead.probability == 100 or lead.iap_enrich_done: continue normalized_email = tools.email_normalize(lead.email_from) if normalized_email: lead_emails[lead.id] = normalized_email.split('@')[1] else: lead.message_post_with_view( 'crm_iap_lead_enrich.mail_message_lead_enrich_no_email', subtype_id=self.env.ref('mail.mt_note').id) if lead_emails: try: iap_response = self.env['iap.enrich.api']._request_enrich(lead_emails) except iap_tools.InsufficientCreditError: _logger.info('Sent batch %s enrich requests: failed because of credit', len(lead_emails)) if not from_cron: data = { 'url': self.env['iap.account'].get_credits_url('reveal'), } self[0].message_post_with_view( 'crm_iap_lead_enrich.mail_message_lead_enrich_no_credit', values=data, subtype_id=self.env.ref('mail.mt_note').id) except Exception as e: _logger.info('Sent batch %s enrich requests: failed with exception %s', len(lead_emails), e) else: _logger.info('Sent batch %s enrich requests: success', len(lead_emails)) self._iap_enrich_from_response(iap_response)
def _get_sync_partner(self, emails): normalized_emails = [ email_normalize(contact) for contact in emails if email_normalize(contact) ] user_partners = self.env['mail.thread']._mail_search_on_user( normalized_emails, extra_domain=[('share', '=', False)]) partners = [user_partner for user_partner in user_partners] remaining = [ email for email in normalized_emails if email not in [partner.email_normalized for partner in partners] ] if remaining: partners += self.env['mail.thread']._mail_find_partner_from_emails( remaining, records=self, force_create=True) return partners
def action_invite(self): """ Process the wizard content and proceed with sending the related email(s), rendering any template patterns on the fly if needed """ self.ensure_one() Partner = self.env['res.partner'] # compute partners and emails, try to find partners for given emails valid_partners = self.partner_ids valid_emails = [] for email in emails_split.split(self.emails or ''): partner = False email_normalized = tools.email_normalize(email) if email_normalized: partner = Partner.search([('email_normalized', '=', email_normalized)]) if partner: valid_partners |= partner else: email_formatted = tools.email_split_and_format(email) if email_formatted: valid_emails.extend(email_formatted) if not valid_partners and not valid_emails: raise UserError(_("Please enter at least one valid recipient.")) answers = self._prepare_answers(valid_partners, valid_emails) for answer in answers: self._send_mail(answer) return {'type': 'ir.actions.act_window_close'}
def _find_members(self, email, partner_id): """Get all the members record corresponding to the email / partner_id. Can be called in batch and return a dictionary {'group_id': <mail.group.member>} Multiple members might have the same email address, but with different partner because there's no unique constraint on the email field of the <res.partner> model. When a partner is given for the search, return in priority - The member whose partner match the given partner - The member without partner but whose email match the given email When no partner is given for the search, return in priority - A member whose email match the given email and has no partner - A member whose email match the given email and has partner """ order = 'partner_id ASC' domain = [('email_normalized', '=', email_normalize(email))] if partner_id: domain = expression.OR([ expression.AND([ [('partner_id', '=', False)], domain, ]), [('partner_id', '=', partner_id)], ]) order = 'partner_id DESC' domain = expression.AND([domain, [('mail_group_id', 'in', self.ids)]]) members_data = self.env['mail.group.member'].sudo().search(domain, order=order) return {member.mail_group_id.id: member for member in members_data}
def groups_index(self, email='', **kw): """View of the group lists. Allow the users to subscribe and unsubscribe.""" if kw.get('group_id') and kw.get('token'): group_id = int(kw.get('group_id')) token = kw.get('token') group = request.env['mail.group'].browse(group_id).exists().sudo() if not group: raise werkzeug.exceptions.NotFound() if token != group._generate_group_access_token(): raise werkzeug.exceptions.NotFound() mail_groups = group else: mail_groups = request.env['mail.group'].search([]).sudo() if not request.env.user._is_public(): # Force the email if the user is logged email_normalized = request.env.user.email_normalized partner_id = request.env.user.partner_id.id else: email_normalized = tools.email_normalize(email) partner_id = None members_data = mail_groups._find_members(email_normalized, partner_id) return request.render('mail_group.mail_groups', { 'mail_groups': [{ 'group': group, 'is_member': bool(members_data.get(group.id, False)), } for group in mail_groups], 'email': email_normalized, 'is_mail_group_manager': request.env.user.has_group('mail_group.group_mail_group_manager'), })
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override _search in order to grep search on email field and make it lower-case and sanitized """ if args: new_args = [] for arg in args: if isinstance( arg, (list, tuple)) and arg[0] == 'email' and isinstance( arg[2], str): normalized = tools.email_normalize(arg[2]) if normalized: new_args.append([arg[0], arg[1], normalized]) else: new_args.append(arg) else: new_args.append(arg) else: new_args = args return super(MailBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
def res_partners_search(self, search_term, limit=30, **kwargs): """ Used for the plugin search contact functionality where the user types a string query in order to search for matching contacts, the string query can either be the name of the contact, it's reference or it's email. We choose these fields because these are probably the most interesting fields that the user can perform a search on. The method returns an array containing the dicts of the matched contacts. """ #In a multi-company environment, the method may return contacts not belonging to the company that the user #is connected to, this may result in the user not being able to view the contact in Odoo, while this may happen #it is not supported for now and users are encouraged to check if they are connected to the correct company before # clicking on a contact. normalized_email = tools.email_normalize(search_term) if normalized_email: filter_domain = [('email_normalized', '=', search_term)] else: filter_domain = [ '|', '|', ('display_name', 'ilike', search_term), ('ref', '=', search_term), ('email', 'ilike', search_term) ] # Search for the partner based on the email. # If multiple are found, take the first one. partners = request.env['res.partner'].search(filter_domain, limit=limit) partners = [self._get_partner_data(partner) for partner in partners] return {"partners": partners}
def find_or_create(self, email, assert_valid_email=False): """ Override to use the email_normalized field. """ if not email: raise ValueError( _('An email is required for find_or_create to work')) parsed_name, parsed_email = self._parse_partner_name(email) if not parsed_email and assert_valid_email: raise ValueError( _('%(email)s is not recognized as a valid email. This is required to create a new customer.' )) if parsed_email: email_normalized = tools.email_normalize(parsed_email) if email_normalized: partners = self.search( [('email_normalized', '=', email_normalized)], limit=1) if partners: return partners # We don't want to call `super()` to avoid searching twice on the email # Especially when the search `email =ilike` cannot be as efficient as # a search on email_normalized with a btree index # If you want to override `find_or_create()` your module should depend on `mail` create_values = {self._rec_name: parsed_name or parsed_email} if parsed_email: # otherwise keep default_email in context create_values['email'] = parsed_email return self.create(create_values)
def _create_user(self): """ create a new user for wizard_user.partner_id :returns record of res.users """ return self.env['res.users'].with_context( no_reset_password=True)._create_user_from_template({ 'email': email_normalize(self.email), 'login': email_normalize(self.email), 'partner_id': self.partner_id.id, 'company_id': self.env.company.id, 'company_ids': [(6, 0, self.env.company.ids)], })
def owner(self, env): # Owner/organizer could be desynchronised between Google and Odoo. # Let userA, userB be two new users (never synced to Google before). # UserA creates an event in Odoo (he is the owner) but userB syncs first. # There is no way to insert the event into userA's calendar since we don't have # any authentication access. The event is therefore inserted into userB's calendar # (he is the organizer in Google). The "real" owner (in Odoo) is stored as an # extended property. There is currently no support to "transfert" ownership when # userA syncs his calendar the first time. real_owner_id = self.extendedProperties and self.extendedProperties.get( 'shared', {}).get('%s_owner_id' % env.cr.dbname) try: # If we create an event without user_id, the event properties will be 'false' # and python will interpret this a a NoneType, that's why we have the 'except TypeError' real_owner_id = int(real_owner_id) except (ValueError, TypeError): real_owner_id = False real_owner = real_owner_id and env['res.users'].browse( real_owner_id) or env['res.users'] if real_owner_id and real_owner.exists(): return real_owner elif self.organizer and self.organizer.get('self'): return env.user elif self.organizer and self.organizer.get('email'): # In Google: 1 email = 1 user; but in Odoo several users might have the same email :/ org_email = email_normalize(self.organizer.get('email')) return env['res.users'].search( [('email_normalized', '=', org_email)], limit=1) else: return env['res.users']
def _parse_partner_name(self, text): """ Parse partner name (given by text) in order to find a name and an email. Supported syntax: * Raoul <*****@*****.**> * "Raoul le Grand" <*****@*****.**> * Raoul [email protected] (strange fault tolerant support from df40926d2a57c101a3e2d221ecfd08fbb4fea30e) Otherwise: default, everything is set as the name. Starting from 13.3 returned email will be normalized to have a coherent encoding. """ name, email = '', '' split_results = tools.email_split_tuples(text) if split_results: name, email = split_results[0] if email and not name: fallback_emails = tools.email_split(text.replace(' ', ',')) if fallback_emails: email = fallback_emails[0] name = text[:text.index(email)].replace('"', '').replace('<', '').strip() if email: email = tools.email_normalize(email) else: name, email = text, '' return name, email
def res_partner_get(self, email=None, name=None, partner_id=None, **kwargs): """ returns a partner given it's id or an email and a name. In case the partner does not exist, we return partner having an id -1, we also look if an existing company matching the contact exists in the database, if none is found a new company is enriched and created automatically old route name "/mail_client_extension/partner/get is deprecated as of saas-14.3, it is not needed for newer versions of the mail plugin but necessary for supporting older versions, only the route name is deprecated not the entire method. """ if not (partner_id or (name and email)): return { 'error': _('You need to specify at least the partner_id or the name and the email' ) } if partner_id: partner = request.env['res.partner'].browse(partner_id) return self._prepare_contact_values(partner) normalized_email = tools.email_normalize(email) if not normalized_email: return {'error': _('Bad Email.')} # Search for the partner based on the email. # If multiple are found, take the first one. partner = request.env['res.partner'].search([ '|', ('email', 'in', [normalized_email, email]), ('email_normalized', '=', normalized_email) ], limit=1) response = self._prepare_contact_values(partner) # if no partner is found in the database, we should also return an empty one having id = -1, otherwise older versions of # plugin won't work if not response['partner']: sender_domain = tools.email_domain_extract(email) response['partner'] = { 'id': -1, 'email': email, 'name': name, 'enrichment_info': None, } company = self._find_existing_company(normalized_email) if not company: # create and enrich company company, enrichment_info = self._create_company_from_iap( sender_domain) response['partner']['enrichment_info'] = enrichment_info response['partner']['company'] = self._prepare_company_values( company) return response
def _remove(self, email): normalized = tools.email_normalize(email) record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)]) if len(record) > 0: record.write({'active': False}) else: record = record.create({'email': email, 'active': False}) return record
def write(self, values): if 'email' in values: email_normalized = email_normalize(values['email']) if not email_normalized: raise UserError( _('Invalid email address %r', values.get('email'))) values['email'] = email_normalized return super(MailGroupModeration, self).write(values)
def _add(self, email): normalized = tools.email_normalize(email) record = self.env["mail.blacklist"].with_context(active_test=False).search([('email', '=', normalized)]) if len(record) > 0: record.action_unarchive() else: record = self.create({'email': email}) return record
def create(self, vals_list): for values in vals_list: email_normalized = email_normalize(values.get('email')) if not email_normalized: raise UserError( _('Invalid email address %r', values.get('email'))) values['email'] = email_normalized return super(MailGroupModeration, self).create(vals_list)
def iap_enrich(self, from_cron=False): # Split self in a list of sub-recordsets or 50 records to prevent timeouts batches = [self[index:index + 50] for index in range(0, len(self), 50)] for leads in batches: lead_emails = {} with self._cr.savepoint(): try: self._cr.execute( "SELECT 1 FROM {} WHERE id in %(lead_ids)s FOR UPDATE NOWAIT".format(self._table), {'lead_ids': tuple(leads.ids)}, log_exceptions=False) for lead in leads: # If lead is lost, active == False, but is anyway removed from the search in the cron. if lead.probability == 100 or lead.iap_enrich_done: continue normalized_email = tools.email_normalize(lead.email_from) if not normalized_email: lead.message_post_with_view( 'crm_iap_enrich.mail_message_lead_enrich_no_email', subtype_id=self.env.ref('mail.mt_note').id) continue email_domain = normalized_email.split('@')[1] # Discard domains of generic email providers as it won't return relevant information if email_domain in iap_tools._MAIL_DOMAIN_BLACKLIST: lead.write({'iap_enrich_done': True}) lead.message_post_with_view( 'crm_iap_enrich.mail_message_lead_enrich_notfound', subtype_id=self.env.ref('mail.mt_note').id) else: lead_emails[lead.id] = email_domain if lead_emails: try: iap_response = self.env['iap.enrich.api']._request_enrich(lead_emails) except iap_tools.InsufficientCreditError: _logger.info('Sent batch %s enrich requests: failed because of credit', len(lead_emails)) if not from_cron: data = { 'url': self.env['iap.account'].get_credits_url('reveal'), } leads[0].message_post_with_view( 'crm_iap_enrich.mail_message_lead_enrich_no_credit', values=data, subtype_id=self.env.ref('mail.mt_note').id) # Since there are no credits left, there is no point to process the other batches break except Exception as e: _logger.info('Sent batch %s enrich requests: failed with exception %s', len(lead_emails), e) else: _logger.info('Sent batch %s enrich requests: success', len(lead_emails)) self._iap_enrich_from_response(iap_response) except OperationalError: _logger.error('A batch of leads could not be enriched :%s', repr(leads)) continue # Commit processed batch to avoid complete rollbacks and therefore losing credits. if not self.env.registry.in_test_mode(): self.env.cr.commit()
def mail_validate(email): global _flanker_lib_warning if not _flanker_lib_warning: _flanker_lib_warning = True _logger.info( "The `flanker` Python module is not installed," "so email validation fallback to email_normalize. Use 'pip install flanker' to install it" ) return tools.email_normalize(email)
def send_feedback(self, mailing_id, res_id, email, feedback, token): mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) if mailing.exists() and email: if not self._valid_unsubscribe_token(mailing_id, res_id, email, token): return 'unauthorized' model = request.env[mailing.mailing_model_real] records = model.sudo().search([('email_normalized', '=', tools.email_normalize(email))]) for record in records: record.sudo().message_post(body=_("Feedback from %s: %s" % (email, feedback))) return bool(records) return 'error'
def _search(self, args, offset=0, limit=None, order=None, count=False, access_rights_uid=None): """ Override _search in order to grep search on email field and make it lower-case and sanitized """ if args: new_args = [] for arg in args: if isinstance(arg, (list, tuple)) and arg[0] == 'email' and isinstance(arg[2], tools.pycompat.text_type): normalized = tools.email_normalize(arg[2]) if normalized: new_args.append([arg[0], arg[1], normalized]) else: new_args.append(arg) else: new_args.append(arg) else: new_args = args return super(MailBlackList, self)._search(new_args, offset=offset, limit=limit, order=order, count=count, access_rights_uid=access_rights_uid)
def update_opt_out(self, email, list_ids, value): if len(list_ids) > 0: model = self.env['mail.mass_mailing.contact'].with_context(active_test=False) records = model.search([('email_normalized', '=', tools.email_normalize(email))]) opt_out_records = self.env['mail.mass_mailing.list_contact_rel'].search([ ('contact_id', 'in', records.ids), ('list_id', 'in', list_ids), ('opt_out', '!=', value) ]) opt_out_records.write({'opt_out': value}) message = _('The recipient <strong>unsubscribed from %s</strong> mailing list(s)') \ if value else _('The recipient <strong>subscribed to %s</strong> mailing list(s)') for record in records: # filter the list_id by record record_lists = opt_out_records.filtered(lambda rec: rec.contact_id.id == record.id) if len(record_lists) > 0: record.sudo().message_post(body=_(message % ', '.join(str(list.name) for list in record_lists.mapped('list_id'))))
def create(self, values): # First of all, extract values to ensure emails are really unique (and don't modify values in place) new_values = [] all_emails = [] for value in values: email = tools.email_normalize(value.get('email')) if not email: raise UserError(_('Invalid email address %r') % value['email']) if email in all_emails: continue all_emails.append(email) new_value = dict(value, email=email) new_values.append(new_value) """ To avoid crash during import due to unique email, return the existing records if any """ sql = '''SELECT email, id FROM mail_blacklist WHERE email = ANY(%s)''' emails = [v['email'] for v in new_values] self._cr.execute(sql, (emails,)) bl_entries = dict(self._cr.fetchall()) to_create = [v for v in new_values if v['email'] not in bl_entries] # TODO DBE Fixme : reorder ids according to incoming ids. results = super(MailBlackList, self).create(to_create) return self.env['mail.blacklist'].browse(bl_entries.values()) | results
def write(self, values): if 'email' in values: values['email'] = tools.email_normalize(values['email']) return super(MailBlackList, self).write(values)
def _compute_email_normalized(self): self._assert_primary_email() for record in self: record.email_normalized = tools.email_normalize(record[self._primary_email])
def _compute_is_email_valid(self): for record in self: normalized = tools.email_normalize(record.email) record.is_email_valid = normalized if not normalized else True
def blacklist_check(self, mailing_id, res_id, email, token): if not self._valid_unsubscribe_token(mailing_id, res_id, email, token): return 'unauthorized' if email: record = request.env['mail.blacklist'].sudo().with_context(active_test=False).search([('email', '=', tools.email_normalize(email))]) if record['active']: return True return False return 'error'
def mailing(self, mailing_id, email=None, res_id=None, token="", **post): mailing = request.env['mail.mass_mailing'].sudo().browse(mailing_id) if mailing.exists(): res_id = res_id and int(res_id) if not self._valid_unsubscribe_token(mailing_id, res_id, email, str(token)): raise exceptions.AccessDenied() if mailing.mailing_model_real == 'mail.mass_mailing.contact': # Unsubscribe directly + Let the user choose his subscriptions mailing.update_opt_out(email, mailing.contact_list_ids.ids, True) contacts = request.env['mail.mass_mailing.contact'].sudo().search([('email_normalized', '=', tools.email_normalize(email))]) subscription_list_ids = contacts.mapped('subscription_list_ids') # In many user are found : if user is opt_out on the list with contact_id 1 but not with contact_id 2, # assume that the user is not opt_out on both # TODO DBE Fixme : Optimise the following to get real opt_out and opt_in opt_out_list_ids = subscription_list_ids.filtered(lambda rel: rel.opt_out).mapped('list_id') opt_in_list_ids = subscription_list_ids.filtered(lambda rel: not rel.opt_out).mapped('list_id') opt_out_list_ids = set([list.id for list in opt_out_list_ids if list not in opt_in_list_ids]) unique_list_ids = set([list.list_id.id for list in subscription_list_ids]) list_ids = request.env['mail.mass_mailing.list'].sudo().browse(unique_list_ids) unsubscribed_list = ', '.join(str(list.name) for list in mailing.contact_list_ids if list.is_public) return request.render('mass_mailing.page_unsubscribe', { 'contacts': contacts, 'list_ids': list_ids, 'opt_out_list_ids': opt_out_list_ids, 'unsubscribed_list': unsubscribed_list, 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param('mass_mailing.show_blacklist_buttons'), }) else: opt_in_lists = request.env['mail.mass_mailing.list_contact_rel'].sudo().search([ ('contact_id.email_normalized', '=', email), ('opt_out', '=', False) ]).mapped('list_id') blacklist_rec = request.env['mail.blacklist'].sudo()._add(email) self._log_blacklist_action( blacklist_rec, mailing_id, _("""Requested blacklisting via unsubscribe link.""")) return request.render('mass_mailing.page_unsubscribed', { 'email': email, 'mailing_id': mailing_id, 'res_id': res_id, 'list_ids': opt_in_lists, 'show_blacklist_button': request.env['ir.config_parameter'].sudo().get_param( 'mass_mailing.show_blacklist_buttons'), }) return request.redirect('/web')
def get_mail_values(self, res_ids): """ Override method that generated the mail content by creating the mail.mail.statistics values in the o2m of mail_mail, when doing pure email mass mailing. """ self.ensure_one() res = super(MailComposeMessage, self).get_mail_values(res_ids) # use only for allowed models in mass mailing if self.composition_mode == 'mass_mail' and \ (self.mass_mailing_name or self.mass_mailing_id) and \ self.env['ir.model'].sudo().search([('model', '=', self.model), ('is_mail_thread', '=', True)], limit=1): mass_mailing = self.mass_mailing_id if not mass_mailing: reply_to_mode = 'email' if self.no_auto_thread else 'thread' reply_to = self.reply_to if self.no_auto_thread else False mass_mailing = self.env['mail.mass_mailing'].create({ 'mass_mailing_campaign_id': self.mass_mailing_campaign_id.id, 'name': self.mass_mailing_name, 'template_id': self.template_id.id, 'state': 'done', 'reply_to_mode': reply_to_mode, 'reply_to': reply_to, 'sent_date': fields.Datetime.now(), 'body_html': self.body, 'mailing_model_id': self.env['ir.model']._get(self.model).id, 'mailing_domain': self.active_domain, }) # Preprocess res.partners to batch-fetch from db # if recipient_ids is present, it means they are partners # (the only object to fill get_default_recipient this way) recipient_partners_ids = [] read_partners = {} for res_id in res_ids: mail_values = res[res_id] if mail_values.get('recipient_ids'): # recipient_ids is a list of x2m command tuples at this point recipient_partners_ids.append(mail_values.get('recipient_ids')[0][1]) read_partners = self.env['res.partner'].browse(recipient_partners_ids) partners_email = {p.id: p.email for p in read_partners} opt_out_list = self._context.get('mass_mailing_opt_out_list') seen_list = self._context.get('mass_mailing_seen_list') mass_mail_layout = self.env.ref('mass_mailing.mass_mailing_mail_layout', raise_if_not_found=False) for res_id in res_ids: mail_values = res[res_id] if mail_values.get('email_to'): mail_to = tools.email_normalize(mail_values['email_to']) else: partner_id = (mail_values.get('recipient_ids') or [(False, '')])[0][1] mail_to = tools.email_normalize(partners_email.get(partner_id)) if (opt_out_list and mail_to in opt_out_list) or (seen_list and mail_to in seen_list) \ or (not mail_to or not email_re.findall(mail_to)): # prevent sending to blocked addresses that were included by mistake mail_values['state'] = 'cancel' elif seen_list is not None: seen_list.add(mail_to) stat_vals = { 'model': self.model, 'res_id': res_id, 'mass_mailing_id': mass_mailing.id, 'email': mail_to, } if mail_values.get('body_html') and mass_mail_layout: mail_values['body_html'] = mass_mail_layout.render({'body': mail_values['body_html']}, engine='ir.qweb', minimal_qcontext=True) # propagate ignored state to stat when still-born if mail_values.get('state') == 'cancel': stat_vals['ignored'] = fields.Datetime.now() mail_values.update({ 'mailing_id': mass_mailing.id, 'statistics_ids': [(0, 0, stat_vals)], # email-mode: keep original message for routing 'notification': mass_mailing.reply_to_mode == 'thread', 'auto_delete': not mass_mailing.keep_archives, }) return res
def _mail_cc_sanitized_raw_dict(self, cc_string): '''return a dict of sanitize_email:raw_email from a string of cc''' if not cc_string: return {} return {tools.email_normalize(email): formataddr((name, tools.email_normalize(email))) for (name, email) in tools.email_split_tuples(cc_string)}