def _parse_import_data(self, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[self.res_model].fields_get() for name, field in all_fields.items(): if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) dt = datetime.datetime server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: # datetime.str[fp]time takes *native strings* in both # versions, for both data and pattern user_format = pycompat.to_native(options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: try: line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index].strip()), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e)) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) return data
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[model].fields_get() for name, field in all_fields.items(): name = prefix + name if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) dt = datetime.datetime server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: # datetime.str[fp]time takes *native strings* in both # versions, for both data and pattern user_format = pycompat.to_native(options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: try: line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index].strip()), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e)) # Check if the field is in import_field and is a relational (followed by /) # Also verify that the field name exactly match the import_field at the correct level. elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields): # Recursive call with the relational as new model and add the field name to the prefix self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) return data
def _adyen_form_get_tx_from_data(self, data): reference, pspReference = data.get('merchantReference'), data.get('pspReference') if not reference or not pspReference: error_msg = _('Adyen: received data with missing reference (%s) or missing pspReference (%s)') % (reference, pspReference) _logger.info(error_msg) raise ValidationError(error_msg) # find tx -> @TDENOTE use pspReference ? tx = self.env['payment.transaction'].search([('reference', '=', reference)]) if not tx or len(tx) > 1: error_msg = _('Adyen: received data for reference %s') % (reference) if not tx: error_msg += _('; no order found') else: error_msg += _('; multiple order found') _logger.info(error_msg) raise ValidationError(error_msg) # verify shasign if len(tx.acquirer_id.adyen_skin_hmac_key) == 64: shasign_check = tx.acquirer_id._adyen_generate_merchant_sig_sha256('out', data) else: shasign_check = tx.acquirer_id._adyen_generate_merchant_sig('out', data) if to_native(shasign_check) != to_native(data.get('merchantSig')): error_msg = _('Adyen: invalid merchantSig, received %s, computed %s') % (data.get('merchantSig'), shasign_check) _logger.warning(error_msg) raise ValidationError(error_msg) return tx
def _adyen_form_get_tx_from_data(self, data): reference, pspReference = data.get('merchantReference'), data.get('pspReference') if not reference or not pspReference: error_msg = _('Adyen: received data with missing reference (%s) or missing pspReference (%s)') % (reference, pspReference) _logger.info(error_msg) raise ValidationError(error_msg) # find tx -> @TDENOTE use pspReference ? tx = self.env['payment.transaction'].search([('reference', '=', reference)]) if not tx or len(tx) > 1: error_msg = _('Adyen: received data for reference %s') % (reference) if not tx: error_msg += _('; no order found') else: error_msg += _('; multiple order found') _logger.info(error_msg) raise ValidationError(error_msg) # verify shasign if len(tx.acquirer_id.adyen_skin_hmac_key) == 64: shasign_check = tx.acquirer_id._adyen_generate_merchant_sig_sha256('out', data) else: shasign_check = tx.acquirer_id._adyen_generate_merchant_sig('out', data) if to_native(shasign_check) != to_native(data.get('merchantSig')): error_msg = _('Adyen: invalid merchantSig, received %s, computed %s') % (data.get('merchantSig'), shasign_check) _logger.warning(error_msg) raise ValidationError(error_msg) return tx
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[model].fields_get() for name, field in all_fields.items(): name = prefix + name if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) dt = datetime.datetime server_format = DEFAULT_SERVER_DATE_FORMAT if field['type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: # datetime.str[fp]time takes *native strings* in both # versions, for both data and pattern user_format = pycompat.to_native(options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: line[index] = line[index].strip() if line[index]: try: line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index]), user_format), server_format) except ValueError as e: try: # Allow to import date in datetime fields if field['type'] == 'datetime': user_format = pycompat.to_native(options.get('date_format')) line[index] = dt.strftime(dt.strptime(pycompat.to_native(line[index]), user_format), server_format) except ValueError as e: raise ValueError(_("Column %s contains incorrect values. Error in line %d: %s") % (name, num + 1, e)) except Exception as e: raise ValueError(_("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e)) # Check if the field is in import_field and is a relational (followed by /) # Also verify that the field name exactly match the import_field at the correct level. elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields): # Recursive call with the relational as new model and add the field name to the prefix self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) elif field['type'] == 'binary' and field.get('attachment') and any(f in name for f in IMAGE_FIELDS) and name in import_fields: index = import_fields.index(name) with requests.Session() as session: session.stream = True for num, line in enumerate(data): if re.match(config.get("import_image_regex", DEFAULT_IMAGE_REGEX), line[index]): if not self.env.user._can_import_remote_urls(): raise AccessError(_("You can not import images via URL, check with your administrator or support for the reason.")) line[index] = self._import_image_by_url(line[index], session, name, num) return data
def _get_date_formats(self): format_date, format_time = super()._get_date_formats() date_format = self.env.user.date_format \ or self.env.user.company_id.date_format if date_format: format_date = pycompat.to_native(date_format) time_format = self.env.user.time_format \ or self.env.user.company_id.time_format if time_format: format_time = pycompat.to_native(time_format) return (format_date, format_time)
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[model].fields_get() for name, field in all_fields.items(): name = prefix + name if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) dt = datetime.datetime server_format = DEFAULT_SERVER_DATE_FORMAT if field[ 'type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: # datetime.str[fp]time takes *native strings* in both # versions, for both data and pattern user_format = pycompat.to_native( options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: line[index] = line[index].strip() if line[index]: try: line[index] = dt.strftime( dt.strptime( pycompat.to_native(line[index]), user_format), server_format) except ValueError as e: raise ValueError( _("Column %s contains incorrect values. Error in line %d: %s" ) % (name, num + 1, e)) except Exception as e: raise ValueError( _("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e)) # Check if the field is in import_field and is a relational (followed by /) # Also verify that the field name exactly match the import_field at the correct level. elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields): # Recursive call with the relational as new model and add the field name to the prefix self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) return data
class CustomModel(models.Model): _name = pycompat.to_native(model_data['model']) _description = model_data['name'] _module = False _custom = True _transient = bool(model_data['transient']) __doc__ = model_data['info']
def encode_rfc2822_address_header(header_text): """If ``header_text`` contains non-ASCII characters, attempts to locate patterns of the form ``"Name" <address@domain>`` and replace the ``"Name"`` portion by the RFC2047-encoded version, preserving the address part untouched. """ def encode_addr(addr): name, email = addr # If s is a <text string>, then charset is a hint specifying the # character set of the characters in the string. The Unicode string # will be encoded using the following charsets in order: us-ascii, # the charset hint, utf-8. The first character set to not provoke a # UnicodeError is used. # -> always pass a text string to Header # also Header.__str__ in Python 3 "Returns an approximation of the # Header as a string, using an unlimited line length.", the old one # was "A synonym for Header.encode()." so call encode() directly? name = Header(pycompat.to_text(name)).encode() # if the from does not follow the (name <addr>),* convention, we might # try to encode meaningless strings as address, as getaddresses is naive # note it would also fail on real addresses with non-ascii characters try: return formataddr((name, email)) except UnicodeEncodeError: _logger.warning(_('Failed to encode the address %s\n' 'from mail header:\n%s') % (addr, header_text)) return "" addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a)
def _remove_gmail_signatures(self, body): if not body: return body try: root = lxml.html.fromstring(body) except ValueError: # In case the email client sent XHTML, fromstring will fail because 'Unicode strings # with encoding declaration are not supported'. root = lxml.html.fromstring(body.encode('utf-8')) postprocessed = False to_remove = [] for node in root.iter(): # TODO: we should probably remove only the first occurence, so # a context var could be used to force remove all if 'gmail_signature' in (node.get('data-smartmail') or ''): postprocessed = True if node.getparent() is not None: to_remove.append(node) for node in to_remove: node.getparent().remove(node) if postprocessed: body = lxml.etree.tostring(root, pretty_print=False, encoding='UTF-8') body = pycompat.to_native(body) return body
def message_parse(self, message, save_original=False): msg_dict = super().message_parse(message, save_original) if not isinstance(message, Message): # message_from_string works on a native str message = pycompat.to_native(message) message = email.message_from_string(message) msg_dict['reply_to'] = tools.decode_smtp_header(message.get('Reply-To')) return msg_dict
class DefaultAerooParser(models.AbstractModel): _inherit = 'report.report_aeroo.abstract' _name = 'report.%s' % pycompat.to_native(model_data['model']) _description = model_data['name'], _module = False _custom = True __doc__ = '' _transient = False
def load_information_from_description_file(module, mod_path=None): """ :param module: The name of the module (sale, purchase, ...) :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...) """ if not mod_path: mod_path = get_module_path(module, downloaded=True) manifest_file = module_manifest(mod_path) if manifest_file: # default values for descriptor info = { 'application': False, 'author': 'Odoo S.A.', 'auto_install': False, 'category': 'Uncategorized', 'depends': [], 'description': '', 'icon': get_module_icon(module), 'installable': True, 'license': 'LGPL-3', 'post_load': None, 'version': '1.0', 'web': False, 'sequence': 100, 'summary': '', 'website': '', } info.update( pycompat.izip( 'depends data demo test init_xml update_xml demo_xml'.split(), iter(list, None))) f = tools.file_open(manifest_file, mode='rb') try: info.update(ast.literal_eval(pycompat.to_native(f.read()))) finally: f.close() if not info.get('description'): readme_path = [ opj(mod_path, x) for x in README if os.path.isfile(opj(mod_path, x)) ] if readme_path: readme_text = tools.file_open(readme_path[0]).read() info['description'] = readme_text if 'active' in info: # 'active' has been renamed 'auto_install' info['auto_install'] = info['active'] info['version'] = adapt_version(info['version']) return info _logger.debug('module %s: no manifest file found %s', module, MANIFEST_NAMES) return {}
def _query(self, conf, filter, retrieve_attributes=None): """ Query an LDAP server with the filter argument and scope subtree. Allow for all authentication methods of the simple authentication method: - authenticated bind (non-empty binddn + valid password) - anonymous bind (empty binddn + empty password) - unauthenticated authentication (non-empty binddn + empty password) .. seealso:: :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method. :param dict conf: LDAP configuration :param filter: valid LDAP filter :param list retrieve_attributes: LDAP attributes to be retrieved. \ If not specified, return all attributes. :return: ldap entries :rtype: list of tuples (dn, attrs) """ results = [] try: conn = self._connect(conf) ldap_password = conf['ldap_password'] or '' ldap_binddn = conf['ldap_binddn'] or '' conn.simple_bind_s(to_native(ldap_binddn), to_native(ldap_password)) results = conn.search_st(to_native(conf['ldap_base']), ldap.SCOPE_SUBTREE, filter, retrieve_attributes, timeout=60) conn.unbind() except ldap.INVALID_CREDENTIALS: _logger.error('LDAP bind failed.') except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return results
def load_information_from_description_file(module, mod_path=None): """ :param module: The name of the module (sale, purchase, ...) :param mod_path: Physical path of module, if not providedThe name of the module (sale, purchase, ...) """ if not mod_path: mod_path = get_module_path(module, downloaded=True) manifest_file = module_manifest(mod_path) if manifest_file: # default values for descriptor info = { 'application': False, 'author': 'Odoo S.A.', 'auto_install': False, 'category': 'Uncategorized', 'depends': [], 'description': '', 'icon': get_module_icon(module), 'installable': True, 'license': 'LGPL-3', 'post_load': None, 'version': '1.0', 'web': False, 'sequence': 100, 'summary': '', 'website': '', } info.update(pycompat.izip( 'depends data demo test init_xml update_xml demo_xml'.split(), iter(list, None))) f = tools.file_open(manifest_file, mode='rb') try: info.update(ast.literal_eval(pycompat.to_native(f.read()))) finally: f.close() if not info.get('description'): readme_path = [opj(mod_path, x) for x in README if os.path.isfile(opj(mod_path, x))] if readme_path: readme_text = tools.file_open(readme_path[0]).read() info['description'] = readme_text if 'active' in info: # 'active' has been renamed 'auto_install' info['auto_install'] = info['active'] info['version'] = adapt_version(info['version']) return info _logger.debug('module %s: no manifest file found %s', module, MANIFEST_NAMES) return {}
def query(self, conf, filter, retrieve_attributes=None): """ Query an LDAP server with the filter argument and scope subtree. Allow for all authentication methods of the simple authentication method: - authenticated bind (non-empty binddn + valid password) - anonymous bind (empty binddn + empty password) - unauthenticated authentication (non-empty binddn + empty password) .. seealso:: :rfc:`4513#section-5.1` - LDAP: Simple Authentication Method. :param dict conf: LDAP configuration :param filter: valid LDAP filter :param list retrieve_attributes: LDAP attributes to be retrieved. \ If not specified, return all attributes. :return: ldap entries :rtype: list of tuples (dn, attrs) """ results = [] try: conn = self.connect(conf) ldap_password = conf['ldap_password'] or '' ldap_binddn = conf['ldap_binddn'] or '' conn.simple_bind_s(to_native(ldap_binddn), to_native(ldap_password)) results = conn.search_st(to_native(conf['ldap_base']), ldap.SCOPE_SUBTREE, filter, retrieve_attributes, timeout=60) conn.unbind() except ldap.INVALID_CREDENTIALS: _logger.error('LDAP bind failed.') except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return results
def encode_rfc2822_address_header(header_text): """If ``header_text`` contains non-ASCII characters, attempts to locate patterns of the form ``"Name" <address@domain>`` and replace the ``"Name"`` portion by the RFC2047-encoded version, preserving the address part untouched. """ def encode_addr(addr): name, email = addr try: return formataddr((name, email), 'ascii') except UnicodeEncodeError: _logger.warning(_('Failed to encode the address %s\n' 'from mail header:\n%s') % (addr, header_text)) return "" addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a)
def authenticate(self, conf, login, password): """ Authenticate a user against the specified LDAP server. In order to prevent an unintended 'unauthenticated authentication', which is an anonymous bind with a valid dn and a blank password, check for empty passwords explicitely (:rfc:`4513#section-6.3.1`) :param dict conf: LDAP configuration :param login: username :param password: Password for the LDAP user :return: LDAP entry of authenticated user or False :rtype: dictionary of attributes """ if not password: return False entry = False try: pattern = r"[^%](%s)" match_count = re.findall(pattern, conf['ldap_filter']).__len__() filter = filter_format(conf['ldap_filter'], (login, ) * match_count) except TypeError: _logger.warning( 'Could not format LDAP filter. Your filter should contain one \'%s\'.' ) return False try: results = self.query(conf, tools.ustr(filter)) # Get rid of (None, attrs) for searchResultReference replies results = [i for i in results if i[0]] if len(results) == 1: dn = results[0][0] conn = self.connect(conf) conn.simple_bind_s(dn, to_native(password)) conn.unbind() entry = results[0] except ldap.INVALID_CREDENTIALS: return False except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return entry
def encode_header(header_text): """Returns an appropriate representation of the given header value, suitable for direct assignment as a header value in an email.message.Message. RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param header_text: unicode or utf-8 encoded string with header value :rtype: string | email.header.Header :return: if ``header_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an email.header.Header that will perform the appropriate RFC2047 encoding of non-ASCII values. """ if not header_text: return "" header_text = ustr(header_text) # FIXME: require unicode higher up? if is_ascii(header_text): return pycompat.to_native(header_text) return Header(header_text, 'utf-8')
def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given header parameter value, suitable for direct assignation as the param value (e.g. via Message.set_param() or Message.add_header()) RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param param_text: unicode or utf-8 encoded string with header value :rtype: string :return: if ``param_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an ASCII string containing the RFC2047 encoded text. """ # For details see the encode_header() method that uses the same logic if not param_text: return "" param_text = ustr(param_text) # FIXME: require unicode higher up? if is_ascii(param_text): return pycompat.to_native(param_text) # TODO: is that actually necessary? return Charset("utf-8").header_encode(param_text)
def encode_header(header_text): """Returns an appropriate representation of the given header value, suitable for direct assignment as a header value in an email.message.Message. RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param header_text: unicode or utf-8 encoded string with header value :rtype: string | email.header.Header :return: if ``header_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an email.header.Header that will perform the appropriate RFC2047 encoding of non-ASCII values. """ if not header_text: return "" header_text = ustr(header_text) # FIXME: require unicode higher up? if is_ascii(header_text): return pycompat.to_native(header_text) return Header(header_text, 'utf-8')
def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given header parameter value, suitable for direct assignation as the param value (e.g. via Message.set_param() or Message.add_header()) RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param param_text: unicode or utf-8 encoded string with header value :rtype: string :return: if ``param_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an ASCII string containing the RFC2047 encoded text. """ # For details see the encode_header() method that uses the same logic if not param_text: return "" param_text = ustr(param_text) # FIXME: require unicode higher up? if is_ascii(param_text): return pycompat.to_native(param_text) # TODO: is that actually necessary? return Charset("utf-8").header_encode(param_text)
def authenticate(self, conf, login, password): """ Authenticate a user against the specified LDAP server. In order to prevent an unintended 'unauthenticated authentication', which is an anonymous bind with a valid dn and a blank password, check for empty passwords explicitely (:rfc:`4513#section-6.3.1`) :param dict conf: LDAP configuration :param login: username :param password: Password for the LDAP user :return: LDAP entry of authenticated user or False :rtype: dictionary of attributes """ if not password: return False entry = False try: filter = filter_format(conf['ldap_filter'], (login,)) except TypeError: _logger.warning('Could not format LDAP filter. Your filter should contain one \'%s\'.') return False try: results = self.query(conf, tools.ustr(filter)) # Get rid of (None, attrs) for searchResultReference replies results = [i for i in results if i[0]] if len(results) == 1: dn = results[0][0] conn = self.connect(conf) conn.simple_bind_s(dn, to_native(password)) conn.unbind() entry = results[0] except ldap.INVALID_CREDENTIALS: return False except ldap.LDAPError as e: _logger.error('An LDAP exception occurred: %s', e) return entry
def encode_rfc2822_address_header(header_text): """If ``header_text`` contains non-ASCII characters, attempts to locate patterns of the form ``"Name" <address@domain>`` and replace the ``"Name"`` portion by the RFC2047-encoded version, preserving the address part untouched. """ def encode_addr(addr): name, email = addr # If s is a <text string>, then charset is a hint specifying the # character set of the characters in the string. The Unicode string # will be encoded using the following charsets in order: us-ascii, # the charset hint, utf-8. The first character set to not provoke a # UnicodeError is used. # -> always pass a text string to Header # also Header.__str__ in Python 3 "Returns an approximation of the # Header as a string, using an unlimited line length.", the old one # was "A synonym for Header.encode()." so call encode() directly? name = Header(pycompat.to_text(name)).encode() return formataddr((name, email)) addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(encode_addr(a) for a in addresses)
def encode_rfc2822_address_header(header_text): """If ``header_text`` contains non-ASCII characters, attempts to locate patterns of the form ``"Name" <address@domain>`` and replace the ``"Name"`` portion by the RFC2047-encoded version, preserving the address part untouched. """ def encode_addr(addr): name, email = addr # If s is a <text string>, then charset is a hint specifying the # character set of the characters in the string. The Unicode string # will be encoded using the following charsets in order: us-ascii, # the charset hint, utf-8. The first character set to not provoke a # UnicodeError is used. # -> always pass a text string to Header # also Header.__str__ in Python 3 "Returns an approximation of the # Header as a string, using an unlimited line length.", the old one # was "A synonym for Header.encode()." so call encode() directly? name = Header(pycompat.to_text(name)).encode() return formataddr((name, email)) addresses = getaddresses([pycompat.to_native(ustr(header_text))]) return COMMASPACE.join(encode_addr(a) for a in addresses)
def _parse_import_data_recursive(self, model, prefix, data, import_fields, options): # Get fields of type date/datetime all_fields = self.env[model].fields_get() for name, field in all_fields.items(): name = prefix + name if field['type'] in ('date', 'datetime') and name in import_fields: # Parse date index = import_fields.index(name) dt = datetime.datetime server_format = DEFAULT_SERVER_DATE_FORMAT if field[ 'type'] == 'date' else DEFAULT_SERVER_DATETIME_FORMAT if options.get('%s_format' % field['type'], server_format) != server_format: # datetime.str[fp]time takes *native strings* in both # versions, for both data and pattern user_format = pycompat.to_native( options.get('%s_format' % field['type'])) for num, line in enumerate(data): if line[index]: line[index] = line[index].strip() if line[index]: try: line[index] = dt.strftime( dt.strptime( pycompat.to_native(line[index]), user_format), server_format) except ValueError as e: try: # Allow to import date in datetime fields if field['type'] == 'datetime': user_format = pycompat.to_native( options.get('date_format')) line[index] = dt.strftime( dt.strptime( pycompat.to_native( line[index]), user_format), server_format) except ValueError as e: raise ValueError( _("Column %s contains incorrect values. Error in line %d: %s" ) % (name, num + 1, e)) except Exception as e: raise ValueError( _("Error Parsing Date [%s:L%d]: %s") % (name, num + 1, e)) # Check if the field is in import_field and is a relational (followed by /) # Also verify that the field name exactly match the import_field at the correct level. elif any(name + '/' in import_field and name == import_field.split('/')[prefix.count('/')] for import_field in import_fields): # Recursive call with the relational as new model and add the field name to the prefix self._parse_import_data_recursive(field['relation'], name + '/', data, import_fields, options) elif field['type'] in ('float', 'monetary') and name in import_fields: # Parse float, sometimes float values from file have currency symbol or () to denote a negative value # We should be able to manage both case index = import_fields.index(name) self._parse_float_from_data(data, index, name, options) elif field['type'] == 'binary' and field.get('attachment') and any( f in name for f in IMAGE_FIELDS) and name in import_fields: index = import_fields.index(name) with requests.Session() as session: session.stream = True for num, line in enumerate(data): if re.match( config.get("import_image_regex", DEFAULT_IMAGE_REGEX), line[index]): if not self.env.user._can_import_remote_urls(): raise AccessError( _("You can not import images via URL, check with your administrator or support for the reason." )) line[index] = self._import_image_by_url( line[index], session, name, num) return data
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, body_alternative=None, subtype_alternative='plain'): """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it. :param string email_from: sender email address :param list email_to: list of recipient addresses (to be joined with commas) :param string subject: email subject (no pre-encoding/quoting necessary) :param string body: email body, of the type ``subtype`` (by default, plaintext). If html subtype is used, the message will be automatically converted to plaintext and wrapped in multipart/alternative, unless an explicit ``body_alternative`` version is passed. :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative`` :param string reply_to: optional value of Reply-To header :param string object_id: optional tracking identifier, to be included in the message-id for recognizing replies. Suggested format for object-id is "res_id-model", e.g. "12345-crm.lead". :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), must match the format of the ``body`` parameter. Default is 'plain', making the content part of the mail "text/plain". :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain' or 'html'). Default is 'plain'. :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string containing the bytes of the attachment :param list email_cc: optional list of string values for CC header (to be joined with commas) :param list email_bcc: optional list of string values for BCC header (to be joined with commas) :param dict headers: optional map of headers to set on the outgoing mail (may override the other headers, including Subject, Reply-To, Message-Id, etc.) :rtype: email.message.Message (usually MIMEMultipart) :return: the new RFC2822 email message """ email_from = email_from or tools.config.get('email_from') assert email_from, "You must either provide a sender address explicitly or configure "\ "a global sender address in the server configuration or with the "\ "--email-from startup parameter." # Note: we must force all strings to to 8-bit utf-8 when crafting message, # or use encode_header() for headers, which does it automatically. headers = headers or {} # need valid dict later email_cc = email_cc or [] email_bcc = email_bcc or [] body = body or u'' email_body = ustr(body) email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: if object_id: message_id = tools.generate_tracking_message_id(object_id) else: message_id = make_msgid() msg['Message-Id'] = encode_header(message_id) if references: msg['references'] = encode_header(references) msg['Subject'] = encode_header(subject) msg['From'] = encode_rfc2822_address_header(email_from) del msg['Reply-To'] if reply_to: msg['Reply-To'] = encode_rfc2822_address_header(reply_to) else: msg['Reply-To'] = msg['From'] msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to)) if email_cc: msg['Cc'] = encode_rfc2822_address_header( COMMASPACE.join(email_cc)) if email_bcc: msg['Bcc'] = encode_rfc2822_address_header( COMMASPACE.join(email_bcc)) msg['Date'] = formatdate() # Custom headers may override normal headers or provide additional ones for key, value in headers.items(): msg[pycompat.to_native(ustr(key))] = encode_header(value) if subtype == 'html' and not body_alternative: # Always provide alternative text body ourselves if possible. text = html2text.html2text(email_body) alternative_part = MIMEMultipart(_subtype="alternative") alternative_part.attach( MIMEText(text, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) elif body_alternative: # Include both alternatives, as specified, within a multipart/alternative part alternative_part = MIMEMultipart(_subtype="alternative") body_alternative_ = ustr(body_alternative) alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8') alternative_part.attach(alternative_body_part) alternative_part.attach(email_text_part) msg.attach(alternative_part) else: msg.attach(email_text_part) if attachments: for (fname, fcontent, mime) in attachments: filename_rfc2047 = encode_header_param(fname) if mime and '/' in mime: maintype, subtype = mime.split('/', 1) part = MIMEBase(maintype, subtype) else: part = MIMEBase('application', "octet-stream") # The default RFC2231 encoding of Message.add_header() works in Thunderbird but not GMail # so we fix it by using RFC2047 encoding for the filename instead. part.set_param('name', filename_rfc2047) part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047) part.set_payload(fcontent) encoders.encode_base64(part) msg.attach(part) return msg
def connect(self, host=None, port=None, user=None, password=None, encryption=None, smtp_debug=False, mail_server_id=None): """Returns a new SMTP connection to the given SMTP server. When running in test mode, this method does nothing and returns `None`. :param host: host or IP of SMTP server to connect to, if mail_server_id not passed :param int port: SMTP port to connect to :param user: optional username to authenticate with :param password: optional password to authenticate with :param string encryption: optional, ``'ssl'`` | ``'starttls'`` :param bool smtp_debug: toggle debugging of SMTP sessions (all i/o will be output in logs) :param mail_server_id: ID of specific mail server to use (overrides other parameters) """ # Do not actually connect while running in test mode if getattr(threading.currentThread(), 'testing', False): return None mail_server = smtp_encryption = None if mail_server_id: mail_server = self.sudo().browse(mail_server_id) elif not host: mail_server = self.sudo().search([], order='sequence', limit=1) if mail_server: smtp_server = mail_server.smtp_host smtp_port = mail_server.smtp_port smtp_user = mail_server.smtp_user smtp_password = mail_server.smtp_pass smtp_encryption = mail_server.smtp_encryption smtp_debug = smtp_debug or mail_server.smtp_debug else: # we were passed individual smtp parameters or nothing and there is no default server smtp_server = host or tools.config.get('smtp_server') smtp_port = tools.config.get('smtp_port', 25) if port is None else port smtp_user = user or tools.config.get('smtp_user') smtp_password = password or tools.config.get('smtp_password') smtp_encryption = encryption if smtp_encryption is None and tools.config.get('smtp_ssl'): smtp_encryption = 'starttls' # smtp_ssl => STARTTLS as of v7 if not smtp_server: raise UserError((_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, " "or provide the SMTP parameters explicitly."))) if smtp_encryption == 'ssl': if 'SMTP_SSL' not in smtplib.__all__: raise UserError( _("Your Odoo Server does not support SMTP-over-SSL. " "You could use STARTTLS instead. " "If SSL is needed, an upgrade to Python 2.6 on the server-side " "should do the trick.")) connection = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=SMTP_TIMEOUT) else: connection = smtplib.SMTP(smtp_server, smtp_port, timeout=SMTP_TIMEOUT) connection.set_debuglevel(smtp_debug) if smtp_encryption == 'starttls': # starttls() will perform ehlo() if needed first # and will discard the previous list of services # after successfully performing STARTTLS command, # (as per RFC 3207) so for example any AUTH # capability that appears only on encrypted channels # will be correctly detected for next step connection.starttls() if smtp_user: # Attempt authentication - will raise if AUTH service not supported # The user/password must be converted to bytestrings in order to be usable for # certain hashing schemes, like HMAC. # See also bug #597143 and python issue #5285 smtp_user = pycompat.to_native(ustr(smtp_user)) smtp_password = pycompat.to_native(ustr(smtp_password)) connection.login(smtp_user, smtp_password) return connection
def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False): """Defines a default value for the given model and field_name. Any previous default for the same scope (model, field_name, value, for_all_users, company_id, condition) will be replaced and lost in the process. Defaults can be later retrieved via :meth:`~.get_defaults`, which will return the highest priority default for any given field. Defaults that are more specific have a higher priority, in the following order (highest to lowest): * specific to user and company * specific to user only * specific to company only * global to everyone :param string model: model name :param string field_name: field name to which the default applies :param value: the default field value to set :type value: any serializable Python value :param bool for_all_users: whether the default should apply to everybody or only the user calling the method :param int company_id: optional ID of the company to which the default should apply. If omitted, the default will be global. If True is passed, the current user's company will be used. :param string condition: optional condition specification that can be used to restrict the applicability of the default values (e.g. based on another field's value). This is an opaque string as far as the API is concerned, but client stacks typically use single-field conditions in the form ``'key=stringified_value'``. (Currently, the condition is trimmed to 200 characters, so values that share the same first 200 characters always match) :return: the newly created ir.values entry """ if isinstance(value, pycompat.string_types): value = pycompat.to_native(value) if company_id is True: # should be company-specific, need to get company id company_id = self.env.user.company_id.id # check consistency of model, field_name and value try: field = self.env[model]._fields[field_name] field.convert_to_cache(value, self.browse()) except KeyError: _logger.warning("Invalid field %s.%s", model, field_name) except Exception: raise ValidationError(_("Invalid value for %s.%s: %s") % (model, field_name, value)) # remove existing defaults for the same scope search_criteria = [ ('key', '=', 'default'), ('key2', '=', condition and condition[:200]), ('model', '=', model), ('name', '=', field_name), ('user_id', '=', False if for_all_users else self._uid), ('company_id', '=', company_id) ] self.search(search_criteria).unlink() return self.create({ 'name': field_name, 'value': pickle.dumps(value), 'model': model, 'key': 'default', 'key2': condition and condition[:200], 'user_id': False if for_all_users else self._uid, 'company_id': company_id, })
class InnerCustomModel(models.Model): _name = pycompat.to_native(model_data['model'])
def fetch_mail(self): for server in self: count, failed = 0, 0 pop_server = None if server.type == 'pop': _logger.info('Server tpye is POP') try: while True: pop_server = server.connect() (num_messages, total_size) = pop_server.stat() pop_server.list() _logger.info('Server tpye is POP inside while') _logger.info('total_size = %d', total_size) _logger.info('num_messages = %d', num_messages) for num in range( 1, min(MAX_POP_MESSAGES, num_messages) + 1): _logger.info( 'Server tpye is POP inside while INSIDE FOR') (header, messages, octets) = pop_server.retr(num) message = (b'\n').join(messages) res_id = None response = { 'errorCode': 100, 'message': 'File Uploaded Successfully' } try: if isinstance(message, xmlrpclib.Binary): message = bytes(message.data) if isinstance(message, pycompat.text_type): message = message.encode('utf-8') extract = getattr(email, 'message_from_bytes', email.message_from_string) message = extract(message) if not isinstance(message, Message): message = pycompat.to_native(message) message = email.message_from_string( message) email_to = tools.decode_message_header( message, 'To') match = re.search(r'[\w\.-]+@[\w\.-]+', email_to) email_to = str(match.group(0)) _logger.info('Email to %r', email_to) # if email_to == INCOMING_EMAIL_ID: _Attachment = namedtuple( 'Attachment', ('fname', 'content', 'info')) attachments = [] body = u'' email_from = tools.decode_message_header( message, 'From') _logger.info('Email from %r', email_from) match = re.search(r'[\w\.-]+@[\w\.-]+', email_from) email_from = str(match.group(0)) subject = tools.decode_message_header( message, 'Subject') tmpl_type = None if 'Inventory' in subject: tmpl_type = "Inventory" elif 'Requirement' in subject: tmpl_type = "Requirement" if message.get_content_maintype() != 'text': alternative = False for part in message.walk(): if part.get_content_type( ) == 'multipart/alternative': alternative = True if part.get_content_maintype( ) == 'multipart': continue # skip container filename = part.get_param( 'filename', None, 'content-disposition') if not filename: filename = part.get_param( 'name', None) if filename: if isinstance(filename, tuple): filename = email.utils.collapse_rfc2231_value( filename).strip() else: filename = tools.decode_smtp_header( filename) encoding = part.get_content_charset() if filename and part.get('content-id'): inner_cid = part.get( 'content-id').strip('><') attachments.append( _Attachment( filename, part.get_payload( decode=True), {'cid': inner_cid})) continue if filename or part.get( 'content-disposition', '' ).strip().startswith('attachment'): attachments.append( _Attachment( filename or 'attachment', part.get_payload( decode=True), {})) continue if part.get_content_type( ) == 'text/plain' and (not alternative or not body): body = tools.append_content_to_html( body, tools.ustr(part.get_payload( decode=True), encoding, errors='replace'), preserve=True) elif part.get_content_type( ) == 'text/html': body = tools.ustr( part.get_payload(decode=True), encoding, errors='replace') else: attachments.append( _Attachment( filename or 'attachment', part.get_payload( decode=True), {})) if len(attachments) > 0: encoding = message.get_content_charset( ) plain_text = html2text.HTML2Text() message_payload = plain_text.handle( tools.ustr(body, encoding, errors='replace')) if '- Forwarded message -' in message_payload: messages = message_payload.split( '- Forwarded message -') _logger.info( 'Forwarded message payload: %r', messages) total_parts = len(messages) originator_part = messages[ total_parts - 1] _logger.info( 'originator_part: %r', originator_part) match = re.search( r'[\w\.-]+@[\w\.-]+', originator_part) _logger.info('match: %r', match) if match: email_from_domain = re.search( "@[\w.]+", email_from).group(0) _logger.info( 'email_from_domain: %r', email_from_domain) email_to_domain = re.search( "@[\w.]+", email_to).group(0) _logger.info( 'email_to_domain: %r', email_to_domain) if email_to_domain != email_from_domain: email_from = None else: email_from = str( match.group(0)) _logger.info( 'email_to_domain email_from: %r', email_from) #_logger.info('message payload: %r %r', message_payload, email_from) if not email_from is None: users_model = self.env[ 'res.partner'].search([ ("email", "=", email_from) ]) if users_model: if len(users_model) == 1: user_attachment_dir = ATTACHMENT_DIR + str( datetime.now( ).strftime("%d%m%Y") ) + "/" + str( users_model.id) + "/" if not os.path.exists( os.path.dirname( user_attachment_dir )): try: os.makedirs( os.path. dirname( user_attachment_dir )) except OSError as exc: if exc.errno != errno.EEXIST: raise for attachment in attachments: filename = getattr( attachment, 'fname') if not filename is None: try: file_contents_bytes = getattr( attachment, 'content') file_path = user_attachment_dir + str( filename) file_ref = open( str(file_path ), "wb+") file_ref.write( file_contents_bytes ) file_ref.close( ) response = self.env[ 'sps.document.process'].process_document( users_model, file_path, tmpl_type, filename, 'Email' ) except Exception as e: _logger.info( str(e)) else: _logger.error( 'Presents Same Email Id for multiple users %r', email_from) response = dict( errorCode=101, message= 'Presents Same Email Id for multiple users : ' + str(email_from)) else: _logger.info( 'user not found for %r', email_from) response = dict( errorCode=102, message= 'User not found for : ' + str(email_from)) else: _logger.info( 'domain not matched for forwarded email' ) response = dict( errorCode=103, message= 'Domain not matched for forwarded email : ' + str(email_from)) else: _logger.info("No attachements found") response = dict( errorCode=104, message='No attachements found : ' + str(email_from)) else: _logger.info('Not a Multipart email') response = dict( errorCode=105, message='Not a Multipart email' + str(email_from)) pop_server.dele(num) if "errorCode" in response: self.send_mail( "Sending Email Response as " + str(response['message']) + " for user " + str(email_from)) except Exception: _logger.info( 'Failed to process mail from %s server %s.', server.type, server.name, exc_info=True) failed += 1 if res_id and server.action_id: server.action_id.with_context({ 'active_id': res_id, 'active_ids': [res_id], 'active_model': self.env.context.get( "thread_model", server.object_id.model) }).run() self.env.cr.commit() _logger.info('num_messages = %d', num_messages) if num_messages < MAX_POP_MESSAGES: break pop_server.quit() _logger.info( "Fetched %d email(s) on %s server %s; %d succeeded, %d failed.", num_messages, server.type, server.name, (num_messages - failed), failed) except Exception: _logger.info( "General failure when trying to fetch mail from %s server %s.", server.type, server.name, exc_info=True) finally: _logger.info('Server tpye is POP inside finally') if pop_server: pop_server.quit() server.write({'date': fields.Datetime.now()}) return super(IncomingMailCronModel, self).fetch_mail()
def connect(self, host=None, port=None, user=None, password=None, encryption=None, smtp_debug=False, mail_server_id=None): """Returns a new SMTP connection to the given SMTP server. When running in test mode, this method does nothing and returns `None`. :param host: host or IP of SMTP server to connect to, if mail_server_id not passed :param int port: SMTP port to connect to :param user: optional username to authenticate with :param password: optional password to authenticate with :param string encryption: optional, ``'ssl'`` | ``'starttls'`` :param bool smtp_debug: toggle debugging of SMTP sessions (all i/o will be output in logs) :param mail_server_id: ID of specific mail server to use (overrides other parameters) """ # Do not actually connect while running in test mode if getattr(threading.currentThread(), 'testing', False): return None mail_server = smtp_encryption = None if mail_server_id: mail_server = self.sudo().browse(mail_server_id) elif not host: mail_server = self.sudo().search([], order='sequence', limit=1) if mail_server: smtp_server = mail_server.smtp_host smtp_port = mail_server.smtp_port smtp_user = mail_server.smtp_user smtp_password = mail_server.smtp_pass smtp_encryption = mail_server.smtp_encryption smtp_debug = smtp_debug or mail_server.smtp_debug else: # we were passed individual smtp parameters or nothing and there is no default server smtp_server = host or tools.config.get('smtp_server') smtp_port = tools.config.get('smtp_port', 25) if port is None else port smtp_user = user or tools.config.get('smtp_user') smtp_password = password or tools.config.get('smtp_password') smtp_encryption = encryption if smtp_encryption is None and tools.config.get('smtp_ssl'): smtp_encryption = 'starttls' # smtp_ssl => STARTTLS as of v7 if not smtp_server: raise UserError( (_("Missing SMTP Server") + "\n" + _("Please define at least one SMTP server, " "or provide the SMTP parameters explicitly."))) if smtp_encryption == 'ssl': if 'SMTP_SSL' not in smtplib.__all__: raise UserError( _("Your Odoo Server does not support SMTP-over-SSL. " "You could use STARTTLS instead. " "If SSL is needed, an upgrade to Python 2.6 on the server-side " "should do the trick.")) connection = smtplib.SMTP_SSL(smtp_server, smtp_port) else: connection = smtplib.SMTP(smtp_server, smtp_port) connection.set_debuglevel(smtp_debug) if smtp_encryption == 'starttls': # starttls() will perform ehlo() if needed first # and will discard the previous list of services # after successfully performing STARTTLS command, # (as per RFC 3207) so for example any AUTH # capability that appears only on encrypted channels # will be correctly detected for next step connection.starttls() if smtp_user: # Attempt authentication - will raise if AUTH service not supported # The user/password must be converted to bytestrings in order to be usable for # certain hashing schemes, like HMAC. # See also bug #597143 and python issue #5285 smtp_user = pycompat.to_native(ustr(smtp_user)) smtp_password = pycompat.to_native(ustr(smtp_password)) connection.login(smtp_user, smtp_password) return connection
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False, attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None, body_alternative=None, subtype_alternative='plain'): """Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it. :param string email_from: sender email address :param list email_to: list of recipient addresses (to be joined with commas) :param string subject: email subject (no pre-encoding/quoting necessary) :param string body: email body, of the type ``subtype`` (by default, plaintext). If html subtype is used, the message will be automatically converted to plaintext and wrapped in multipart/alternative, unless an explicit ``body_alternative`` version is passed. :param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative`` :param string reply_to: optional value of Reply-To header :param string object_id: optional tracking identifier, to be included in the message-id for recognizing replies. Suggested format for object-id is "res_id-model", e.g. "12345-crm.lead". :param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'), must match the format of the ``body`` parameter. Default is 'plain', making the content part of the mail "text/plain". :param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain' or 'html'). Default is 'plain'. :param list attachments: list of (filename, filecontents) pairs, where filecontents is a string containing the bytes of the attachment :param list email_cc: optional list of string values for CC header (to be joined with commas) :param list email_bcc: optional list of string values for BCC header (to be joined with commas) :param dict headers: optional map of headers to set on the outgoing mail (may override the other headers, including Subject, Reply-To, Message-Id, etc.) :rtype: email.message.Message (usually MIMEMultipart) :return: the new RFC2822 email message """ email_from = email_from or tools.config.get('email_from') assert email_from, "You must either provide a sender address explicitly or configure "\ "a global sender address in the server configuration or with the "\ "--email-from startup parameter." # Note: we must force all strings to to 8-bit utf-8 when crafting message, # or use encode_header() for headers, which does it automatically. headers = headers or {} # need valid dict later email_cc = email_cc or [] email_bcc = email_bcc or [] body = body or u'' email_body = ustr(body) email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8') msg = MIMEMultipart() if not message_id: if object_id: message_id = tools.generate_tracking_message_id(object_id) else: message_id = make_msgid() msg['Message-Id'] = encode_header(message_id) if references: msg['references'] = encode_header(references) msg['Subject'] = encode_header(subject) msg['From'] = encode_rfc2822_address_header(email_from) del msg['Reply-To'] if reply_to: msg['Reply-To'] = encode_rfc2822_address_header(reply_to) else: msg['Reply-To'] = msg['From'] msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to)) if email_cc: msg['Cc'] = encode_rfc2822_address_header(COMMASPACE.join(email_cc)) if email_bcc: msg['Bcc'] = encode_rfc2822_address_header(COMMASPACE.join(email_bcc)) msg['Date'] = formatdate() # Custom headers may override normal headers or provide additional ones for key, value in headers.items(): msg[pycompat.to_native(ustr(key))] = encode_header(value) if subtype == 'html' and not body_alternative: # Always provide alternative text body ourselves if possible. text = html2text.html2text(email_body) alternative_part = MIMEMultipart(_subtype="alternative") alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain')) alternative_part.attach(email_text_part) msg.attach(alternative_part) elif body_alternative: # Include both alternatives, as specified, within a multipart/alternative part alternative_part = MIMEMultipart(_subtype="alternative") body_alternative_ = ustr(body_alternative) alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8') alternative_part.attach(alternative_body_part) alternative_part.attach(email_text_part) msg.attach(alternative_part) else: msg.attach(email_text_part) if attachments: for (fname, fcontent, mime) in attachments: filename_rfc2047 = encode_header_param(fname) if mime and '/' in mime: maintype, subtype = mime.split('/', 1) part = MIMEBase(maintype, subtype) else: part = MIMEBase('application', "octet-stream") # The default RFC2231 encoding of Message.add_header() works in Thunderbird but not GMail # so we fix it by using RFC2047 encoding for the filename instead. part.set_param('name', filename_rfc2047) part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047) part.set_payload(fcontent) encoders.encode_base64(part) msg.attach(part) return msg
def set_default(self, model, field_name, value, for_all_users=True, company_id=False, condition=False): """Defines a default value for the given model and field_name. Any previous default for the same scope (model, field_name, value, for_all_users, company_id, condition) will be replaced and lost in the process. Defaults can be later retrieved via :meth:`~.get_defaults`, which will return the highest priority default for any given field. Defaults that are more specific have a higher priority, in the following order (highest to lowest): * specific to user and company * specific to user only * specific to company only * global to everyone :param string model: model name :param string field_name: field name to which the default applies :param value: the default field value to set :type value: any serializable Python value :param bool for_all_users: whether the default should apply to everybody or only the user calling the method :param int company_id: optional ID of the company to which the default should apply. If omitted, the default will be global. If True is passed, the current user's company will be used. :param string condition: optional condition specification that can be used to restrict the applicability of the default values (e.g. based on another field's value). This is an opaque string as far as the API is concerned, but client stacks typically use single-field conditions in the form ``'key=stringified_value'``. (Currently, the condition is trimmed to 200 characters, so values that share the same first 200 characters always match) :return: the newly created ir.values entry """ if isinstance(value, pycompat.string_types): value = pycompat.to_native(value) if company_id is True: # should be company-specific, need to get company id company_id = self.env.user.company_id.id # check consistency of model, field_name and value try: field = self.env[model]._fields[field_name] field.convert_to_cache(value, self.browse()) except KeyError: _logger.warning("Invalid field %s.%s", model, field_name) except Exception: raise ValidationError( _("Invalid value for %s.%s: %s") % (model, field_name, value)) # remove existing defaults for the same scope search_criteria = [('key', '=', 'default'), ('key2', '=', condition and condition[:200]), ('model', '=', model), ('name', '=', field_name), ('user_id', '=', False if for_all_users else self._uid), ('company_id', '=', company_id)] self.search(search_criteria).unlink() return self.create({ 'name': field_name, 'value': pickle.dumps(value), 'model': model, 'key': 'default', 'key2': condition and condition[:200], 'user_id': False if for_all_users else self._uid, 'company_id': company_id, })