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
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_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 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 actpy 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