def prepare_email_message(self): """ Returns a django ``EmailMessage`` or ``EmailMultiAlternatives`` object, depending on whether html_message is empty. """ if get_override_recipients(): self.to = get_override_recipients() if self.template is not None: engine = get_template_engine() subject = engine.from_string(self.template.subject).render( self.context) plaintext_message = engine.from_string( self.template.content).render(self.context) multipart_template = engine.from_string(self.template.html_content) html_message = multipart_template.render(self.context) else: subject = smart_str(self.subject) plaintext_message = self.message multipart_template = None html_message = self.html_message connection = connections[self.backend_alias or 'default'] if html_message: if plaintext_message: msg = EmailMultiAlternatives(subject=subject, body=plaintext_message, from_email=self.from_email, to=self.to, bcc=self.bcc, cc=self.cc, headers=self.headers, connection=connection) msg.attach_alternative(html_message, "text/html") else: msg = EmailMultiAlternatives(subject=subject, body=html_message, from_email=self.from_email, to=self.to, bcc=self.bcc, cc=self.cc, headers=self.headers, connection=connection) msg.content_subtype = 'html' if hasattr(multipart_template, 'attach_related'): multipart_template.attach_related(msg) else: msg = EmailMessage(subject=subject, body=plaintext_message, from_email=self.from_email, to=self.to, bcc=self.bcc, cc=self.cc, headers=self.headers, connection=connection) for attachment in self.attachments.all(): if attachment.headers: mime_part = MIMENonMultipart(*attachment.mimetype.split('/')) mime_part.set_payload(attachment.file.read()) for key, val in attachment.headers.items(): try: mime_part.replace_header(key, val) except KeyError: mime_part.add_header(key, val) msg.attach(mime_part) else: msg.attach(attachment.name, attachment.file.read(), mimetype=attachment.mimetype or None) attachment.file.close() self._cached_email_message = msg return msg
def method(self, **kwargs): # Don't bother with doc string, it will be over-written by createMethod. for name in kwargs.iterkeys(): if name not in parameters.argmap: raise TypeError('Got an unexpected keyword argument "%s"' % name) # Remove args that have a value of None. keys = kwargs.keys() for name in keys: if kwargs[name] is None: del kwargs[name] for name in parameters.required_params: if name not in kwargs: raise TypeError('Missing required parameter "%s"' % name) for name, regex in parameters.pattern_params.iteritems(): if name in kwargs: if isinstance(kwargs[name], basestring): pvalues = [kwargs[name]] else: pvalues = kwargs[name] for pvalue in pvalues: if re.match(regex, pvalue) is None: raise TypeError( 'Parameter "%s" value "%s" does not match the pattern "%s"' % (name, pvalue, regex)) for name, enums in parameters.enum_params.iteritems(): if name in kwargs: # We need to handle the case of a repeated enum # name differently, since we want to handle both # arg='value' and arg=['value1', 'value2'] if (name in parameters.repeated_params and not isinstance(kwargs[name], basestring)): values = kwargs[name] else: values = [kwargs[name]] for value in values: if value not in enums: raise TypeError( 'Parameter "%s" value "%s" is not an allowed value in "%s"' % (name, value, str(enums))) actual_query_params = {} actual_path_params = {} for key, value in kwargs.iteritems(): to_type = parameters.param_types.get(key, 'string') # For repeated parameters we cast each member of the list. if key in parameters.repeated_params and type(value) == type([]): cast_value = [_cast(x, to_type) for x in value] else: cast_value = _cast(value, to_type) if key in parameters.query_params: actual_query_params[parameters.argmap[key]] = cast_value if key in parameters.path_params: actual_path_params[parameters.argmap[key]] = cast_value body_value = kwargs.get('body', None) media_filename = kwargs.get('media_body', None) if self._developerKey: actual_query_params['key'] = self._developerKey model = self._model if methodName.endswith('_media'): model = MediaModel() elif 'response' not in methodDesc: model = RawModel() headers = {} headers, params, query, body = model.request(headers, actual_path_params, actual_query_params, body_value) expanded_url = uritemplate.expand(pathUrl, params) url = urlparse.urljoin(self._baseUrl, expanded_url + query) resumable = None multipart_boundary = '' if media_filename: # Ensure we end up with a valid MediaUpload object. if isinstance(media_filename, basestring): (media_mime_type, encoding) = mimetypes.guess_type(media_filename) if media_mime_type is None: raise UnknownFileType(media_filename) if not mimeparse.best_match([media_mime_type], ','.join(accept)): raise UnacceptableMimeTypeError(media_mime_type) media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) elif isinstance(media_filename, MediaUpload): media_upload = media_filename else: raise TypeError('media_filename must be str or MediaUpload.') # Check the maxSize if maxSize > 0 and media_upload.size() > maxSize: raise MediaUploadSizeError("Media larger than: %s" % maxSize) # Use the media path uri for media uploads expanded_url = uritemplate.expand(mediaPathUrl, params) url = urlparse.urljoin(self._baseUrl, expanded_url + query) if media_upload.resumable(): url = _add_query_parameter(url, 'uploadType', 'resumable') if media_upload.resumable(): # This is all we need to do for resumable, if the body exists it gets # sent in the first request, otherwise an empty body is sent. resumable = media_upload else: # A non-resumable upload if body is None: # This is a simple media upload headers['content-type'] = media_upload.mimetype() body = media_upload.getbytes(0, media_upload.size()) url = _add_query_parameter(url, 'uploadType', 'media') else: # This is a multipart/related upload. msgRoot = MIMEMultipart('related') # msgRoot should not write out it's own headers setattr(msgRoot, '_write_headers', lambda self: None) # attach the body as one part msg = MIMENonMultipart(*headers['content-type'].split('/')) msg.set_payload(body) msgRoot.attach(msg) # attach the media as the second part msg = MIMENonMultipart(*media_upload.mimetype().split('/')) msg['Content-Transfer-Encoding'] = 'binary' payload = media_upload.getbytes(0, media_upload.size()) msg.set_payload(payload) msgRoot.attach(msg) # encode the body: note that we can't use `as_string`, because # it plays games with `From ` lines. fp = StringIO.StringIO() g = Generator(fp, mangle_from_=False) g.flatten(msgRoot, unixfrom=False) body = fp.getvalue() multipart_boundary = msgRoot.get_boundary() headers['content-type'] = ( 'multipart/related; ' 'boundary="%s"') % multipart_boundary url = _add_query_parameter(url, 'uploadType', 'multipart') logger.info('URL being requested: %s %s' % (httpMethod, url)) return self._requestBuilder(self._http, model.response, url, method=httpMethod, body=body, headers=headers, methodId=methodId, resumable=resumable)
def _process_msg(self, base_msg, append_msg): def find_ctype(payload): return handlers.type_from_starts_with(payload) for part in base_msg.walk(): if is_skippable(part): continue ctype = None ctype_orig = part.get_content_type() payload = util.fully_decoded_payload(part) was_compressed = False # When the message states it is of a gzipped content type ensure # that we attempt to decode said payload so that the decompressed # data can be examined (instead of the compressed data). if ctype_orig in DECOMP_TYPES: try: payload = util.decomp_gzip(payload, quiet=False) # At this point we don't know what the content-type is # since we just decompressed it. ctype_orig = None was_compressed = True except util.DecompressionError as e: error_message = ("Failed decompressing payload from {} of" " length {} due to: {}".format( ctype_orig, len(payload), e)) _handle_error(error_message, e) continue # Attempt to figure out the payloads content-type if not ctype_orig: ctype_orig = UNDEF_TYPE # There are known cases where mime-type text/x-shellscript included # non shell-script content that was user-data instead. It is safe # to check the true MIME type for x-shellscript type since all # shellscript payloads must have a #! header. The other MIME types # that cloud-init supports do not have the same guarantee. if ctype_orig in TYPE_NEEDED + ['text/x-shellscript']: ctype = find_ctype(payload) if ctype is None: ctype = ctype_orig # In the case where the data was compressed, we want to make sure # that we create a new message that contains the found content # type with the uncompressed content since later traversals of the # messages will expect a part not compressed. if was_compressed: maintype, subtype = ctype.split("/", 1) n_part = MIMENonMultipart(maintype, subtype) n_part.set_payload(payload) # Copy various headers from the old part to the new one, # but don't include all the headers since some are not useful # after decoding and decompression. if part.get_filename(): _set_filename(n_part, part.get_filename()) for h in ('Launch-Index', ): if h in part: _replace_header(n_part, h, str(part[h])) part = n_part if ctype != ctype_orig: _replace_header(part, CONTENT_TYPE, ctype) if ctype in INCLUDE_TYPES: self._do_include(payload, append_msg) continue if ctype in ARCHIVE_TYPES: self._explode_archive(payload, append_msg) continue # TODO(harlowja): Should this be happening, shouldn't # the part header be modified and not the base? _replace_header(base_msg, CONTENT_TYPE, ctype) self._attach_part(append_msg, part)
def send_file(self, filename, addresses, original=None, mime_type='text/plain', rotate_date=None, charset=None): ''' Mails the file with the given file name as an attachement to the given recipient(s). Raises a LogRotateMailerError on harder errors. @param filename: The file name of the file to send (the existing, rotated and maybe compressed logfile). @type filename: str @param addresses: A list of tuples of a pair in the form of the return value of email.utils.parseaddr() @type addresses: list @param original: The file name of the original (unrotated) logfile for informational purposes. If not given, filename is used instead. @type original: str or None @param mime_type: MIME type (content type) of the original logfile, defaults to 'text/plain' @type mime_type: str @param rotate_date: datetime object of rotation, defaults to now() @type rotate_date: datetime or None @param charset: character set of (uncompreesed) logfile, if the mime_type is 'text/plain', defaults to 'utf-8' @type charset: str or None @return: success of sending @rtype: bool ''' _ = self.t.lgettext if not os.path.exists(filename): msg = _("File '%s' doesn't exists.") % (filename) self.logger.error(msg) return False if not os.path.isfile(filename): msg = _("File '%s' is not a regular file.") % (filename) self.logger.warning(msg) return False basename = os.path.basename(filename) if not original: original = os.path.abspath(filename) if not rotate_date: rotate_date = datetime.now() to_list = [] for address in addresses: to_list.append(address[1]) msg = (_("Sending mail with attached file '%(file)s' to: %(rcpt)s") % { 'file': basename, 'rcpt': ', '.join( map(lambda x: ('"' + email.utils.formataddr(x) + '"'), addresses)) }) self.logger.debug(msg) mail_container = MIMEMultipart() mail_container['Date'] = email.utils.formatdate() mail_container['X-Mailer'] = ("pylogrotate version %s" % (self.mailer_version)) mail_container['From'] = self.from_address mail_container['To'] = ', '.join( map(lambda x: email.utils.formataddr(x), addresses)) mail_container['Subject'] = ("Rotated logfile '%s'" % (filename)) mail_container.preamble = ( 'You will not see this in a MIME-aware mail reader.\n') # Generate Text of the first part of mail body mailtext = "Rotated Logfile:\n\n" mailtext += "\t - " + filename + "\n" mailtext += "\t (" + original + ")\n" mailtext += "\n" mailtext += "Date of rotation: " + rotate_date.isoformat(' ') mailtext += "\n" mailtext = _encodestring(mailtext, quotetabs=False) mail_part = MIMENonMultipart('text', 'plain', charset=sys.getdefaultencoding()) mail_part.set_payload(mailtext) mail_part['Content-Transfer-Encoding'] = 'quoted-printable' mail_container.attach(mail_part) ctype, encoding = mimetypes.guess_type(filename) if self.verbose > 3: msg = (_("Guessed content-type: '%(ctype)s' " + "and encoding '%(encoding)s'.") % { 'ctype': ctype, 'encoding': encoding }) self.logger.debug(msg) if encoding: if encoding == 'gzip': ctype = 'application/x-gzip' elif encoding == 'bzip2': ctype = 'application/x-bzip2' else: ctype = 'application/octet-stream' if not ctype: ctype = mime_type maintype, subtype = ctype.split('/', 1) fp = open(filename, 'rb') mail_part = MIMEBase(maintype, subtype) mail_part.set_payload(fp.read()) fp.close() if maintype == 'text': msgtext = mail_part.get_payload() msgtext = _encodestring(msgtext, quotetabs=False) mail_part.set_payload(msgtext) mail_part['Content-Transfer-Encoding'] = 'quoted-printable' else: encoders.encode_base64(mail_part) mail_part.add_header('Content-Disposition', 'attachment', filename=basename) mail_container.attach(mail_part) composed = mail_container.as_string() if self.verbose > 4: msg = _("Generated E-mail:") + "\n" + composed self.logger.debug(msg) if (not self.use_smtp) and self.sendmail: return self._send_per_sendmail(composed) else: return self._send_per_smtp(composed, to_list) return True
def build_email(mail_to, subject, body_text, mail_from=u'*****@*****.**', reply_to=None, attachments=None, mime_headers=None): """ Flexible Multipart MIME message builder. Implements message building using the "email" Python standard library https://docs.python.org/2/library/email-examples.html while following recommendations from https://stackoverflow.com/questions/3902455/smtp-multipart-alternative-vs-multipart-mixed .. seealso:: - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52243 - http://mail.python.org/pipermail/tutor/1999-June/000259.html - http://www.bigbold.com/snippets/posts/show/2038 """ attachments = attachments or {} mime_headers = mime_headers or {} # ------------------------------------------ # envelope # ------------------------------------------ message = MIMEMultipart('mixed') # TODO: Add text for non MIME-aware MUAs #message.preamble = 'You will not see this in a MIME-aware mail reader.\n' # Address headers address_headers = { 'To': fix_addresslist(AddressList(mail_to)), 'From': AddressList(mail_from), 'Reply-To': AddressList(reply_to), } # Subject header mime_headers.update({u'Subject': Header(s=subject, charset='utf-8')}) # Add address headers for key, item in address_headers.iteritems(): if isinstance(item, AddressList): # v1 #for address in format_addresslist(item): # message[key] = address # v2 value = ', '.join(format_addresslist(item)) if value: message[key] = value # Add more headers for key, value in mime_headers.iteritems(): #message.add_header(key, value) if value: message[key] = value # ------------------------------------------ # email body # ------------------------------------------ body_message = MIMEMultipart('alternative') # Start off with a text/plain part # https://stackoverflow.com/questions/3902455/smtp-multipart-alternative-vs-multipart-mixed body_part1 = MIMEText(body_text, _subtype='plain', _charset='utf-8') #body_part1.set_payload(body_text) #body_part1.set_charset('utf-8') body_message.attach(body_part1) message.attach(body_message) # TODO: Add a text/html part #body_part2 = MIMEText(body_html, 'html') # ------------------------------------------ # multipart attachments # ------------------------------------------ # from https://docs.python.org/2/library/email-examples.html for filename, payload in attachments.iteritems(): # Guess the content type based on the file's extension. Encoding # will be ignored, although we should check for simple things like # gzip'd or compressed files. ctype, encoding = mimetypes.guess_type(filename, strict=False) #print('ctype, encoding:', ctype, encoding) if ctype is None or encoding is not None: # No guess could be made, or the file is encoded (compressed), so # use a generic bag-of-bits type. ctype = 'application/octet-stream' maintype, subtype = ctype.split('/', 1) #print('maintype, subtype:', maintype, subtype) # Create proper MIME part by maintype if maintype == 'application' and subtype in ['xml', 'json']: part = MIMENonMultipart(maintype, subtype, charset='utf-8') part.set_payload(payload.encode('utf-8'), 'utf-8') elif maintype == 'text': part = MIMEText(payload.encode('utf-8'), _subtype=subtype, _charset='utf-8') #part.set_charset('utf-8') elif maintype == 'image': part = MIMEImage(payload, _subtype=subtype) elif maintype == 'audio': part = MIMEAudio(payload, _subtype=subtype) else: part = MIMEBase(maintype, subtype) part.set_payload(payload) # Encode the payload using Base64 (Content-Transfer-Encoding) encoders.encode_base64(part) #part = MIMEBase(maintype, subtype, _charset='utf-8') # replace forward slashes by dashes filename_attachment = filename.lstrip('/\\').replace('/', '-') part.add_header('Content-Disposition', 'attachment', filename=filename_attachment.encode('utf-8')) #part.set_payload(payload.encode('utf-8')) #part.set_charset('utf-8') # Encode the payload using Base64 (Content-Transfer-Encoding) #encoders.encode_base64(part) # Add part to multipart message message.attach(part) payload = message.as_string() return payload
def send(cls, to='', cc='', bcc='', subject='', body='', files=None, record=None, reports=None, attachments=None): pool = Pool() User = pool.get('res.user') ActionReport = pool.get('ir.action.report') Attachment = pool.get('ir.attachment') transaction = Transaction() user = User(transaction.user) Model = pool.get(record[0]) record = Model(record[1]) body_html = HTML_EMAIL % { 'subject': subject, 'body': body, 'signature': user.signature or '', } content = MIMEMultipart('alternative') if html2text: body_text = HTML_EMAIL % { 'subject': subject, 'body': body, 'signature': '', } converter = html2text.HTML2Text() body_text = converter.handle(body_text) if user.signature: body_text += '\n-- \n' + converter.handle(user.signature) part = MIMEText(body_text, 'plain', _charset='utf-8') content.attach(part) part = MIMEText(body_html, 'html', _charset='utf-8') content.attach(part) if files or reports or attachments: msg = MIMEMultipart('mixed') msg.attach(content) if files is None: files = [] else: files = list(files) for report_id in (reports or []): report = ActionReport(report_id) Report = pool.get(report.report_name, type='report') ext, content, _, title = Report.execute([record.id], { 'action_id': report.id, }) name = '%s.%s' % (title, ext) if isinstance(content, str): content = content.encode('utf-8') files.append((name, content)) if attachments: files += [(a.name, a.data) for a in Attachment.browse(attachments)] for name, data in files: mimetype, _ = mimetypes.guess_type(name) if mimetype: attachment = MIMENonMultipart(*mimetype.split('/')) attachment.set_payload(data) encode_base64(attachment) else: attachment = MIMEApplication(data) attachment.add_header('Content-Disposition', 'attachment', filename=('utf-8', '', name)) msg.attach(attachment) else: msg = content msg['From'] = from_ = config.get('email', 'from') if user.email: if user.name: user_email = formataddr((user.name, user.email)) else: user_email = user.email msg['Behalf-Of'] = user_email msg['Reply-To'] = user_email msg['To'] = ', '.join(formataddr(a) for a in getaddresses([to])) msg['Cc'] = ', '.join(formataddr(a) for a in getaddresses([cc])) msg['Subject'] = Header(subject, 'utf-8') to_addrs = list( filter( None, map(str.strip, _get_emails(to) + _get_emails(cc) + _get_emails(bcc)))) sendmail_transactional(from_, to_addrs, msg, datamanager=SMTPDataManager(strict=True)) email = cls(recipients=to, recipients_secondary=cc, recipients_hidden=bcc, addresses=[{ 'address': a } for a in to_addrs], subject=subject, body=body, resource=record) email.save() with Transaction().set_context(_check_access=False): attachments_ = [] for name, data in files: attachments_.append( Attachment(resource=email, name=name, data=data)) Attachment.save(attachments_) return email
def method(self, **kwargs): # Don't bother with doc string, it will be over-written by createMethod. for name in six.iterkeys(kwargs): if name not in parameters.argmap: raise TypeError('Got an unexpected keyword argument "%s"' % name) # Remove args that have a value of None. keys = list(kwargs.keys()) for name in keys: if kwargs[name] is None: del kwargs[name] for name in parameters.required_params: if name not in kwargs: # temporary workaround for non-paging methods incorrectly requiring # page token parameter (cf. drive.changes.watch vs. drive.changes.list) if name not in _PAGE_TOKEN_NAMES or _findPageTokenName( _methodProperties(methodDesc, schema, "response")): raise TypeError('Missing required parameter "%s"' % name) for name, regex in six.iteritems(parameters.pattern_params): if name in kwargs: if isinstance(kwargs[name], six.string_types): pvalues = [kwargs[name]] else: pvalues = kwargs[name] for pvalue in pvalues: if re.match(regex, pvalue) is None: raise TypeError( 'Parameter "%s" value "%s" does not match the pattern "%s"' % (name, pvalue, regex)) for name, enums in six.iteritems(parameters.enum_params): if name in kwargs: # We need to handle the case of a repeated enum # name differently, since we want to handle both # arg='value' and arg=['value1', 'value2'] if name in parameters.repeated_params and not isinstance( kwargs[name], six.string_types): values = kwargs[name] else: values = [kwargs[name]] for value in values: if value not in enums: raise TypeError( 'Parameter "%s" value "%s" is not an allowed value in "%s"' % (name, value, str(enums))) actual_query_params = {} actual_path_params = {} for key, value in six.iteritems(kwargs): to_type = parameters.param_types.get(key, "string") # For repeated parameters we cast each member of the list. if key in parameters.repeated_params and type(value) == type([]): cast_value = [_cast(x, to_type) for x in value] else: cast_value = _cast(value, to_type) if key in parameters.query_params: actual_query_params[parameters.argmap[key]] = cast_value if key in parameters.path_params: actual_path_params[parameters.argmap[key]] = cast_value body_value = kwargs.get("body", None) media_filename = kwargs.get("media_body", None) media_mime_type = kwargs.get("media_mime_type", None) if self._developerKey: actual_query_params["key"] = self._developerKey model = self._model if methodName.endswith("_media"): model = MediaModel() elif "response" not in methodDesc: model = RawModel() headers = {} headers, params, query, body = model.request(headers, actual_path_params, actual_query_params, body_value) expanded_url = uritemplate.expand(pathUrl, params) url = _urljoin(self._baseUrl, expanded_url + query) resumable = None multipart_boundary = "" if media_filename: # Ensure we end up with a valid MediaUpload object. if isinstance(media_filename, six.string_types): if media_mime_type is None: logger.warning( "media_mime_type argument not specified: trying to auto-detect for %s", media_filename, ) media_mime_type, _ = mimetypes.guess_type(media_filename) if media_mime_type is None: raise UnknownFileType(media_filename) if not mimeparse.best_match([media_mime_type], ",".join(accept)): raise UnacceptableMimeTypeError(media_mime_type) media_upload = MediaFileUpload(media_filename, mimetype=media_mime_type) elif isinstance(media_filename, MediaUpload): media_upload = media_filename else: raise TypeError("media_filename must be str or MediaUpload.") # Check the maxSize if media_upload.size( ) is not None and media_upload.size() > maxSize > 0: raise MediaUploadSizeError("Media larger than: %s" % maxSize) # Use the media path uri for media uploads expanded_url = uritemplate.expand(mediaPathUrl, params) url = _urljoin(self._baseUrl, expanded_url + query) if media_upload.resumable(): url = _add_query_parameter(url, "uploadType", "resumable") if media_upload.resumable(): # This is all we need to do for resumable, if the body exists it gets # sent in the first request, otherwise an empty body is sent. resumable = media_upload else: # A non-resumable upload if body is None: # This is a simple media upload headers["content-type"] = media_upload.mimetype() body = media_upload.getbytes(0, media_upload.size()) url = _add_query_parameter(url, "uploadType", "media") else: # This is a multipart/related upload. msgRoot = MIMEMultipart("related") # msgRoot should not write out it's own headers setattr(msgRoot, "_write_headers", lambda self: None) # attach the body as one part msg = MIMENonMultipart(*headers["content-type"].split("/")) msg.set_payload(body) msgRoot.attach(msg) # attach the media as the second part msg = MIMENonMultipart(*media_upload.mimetype().split("/")) msg["Content-Transfer-Encoding"] = "binary" payload = media_upload.getbytes(0, media_upload.size()) msg.set_payload(payload) msgRoot.attach(msg) # encode the body: note that we can't use `as_string`, because # it plays games with `From ` lines. fp = BytesIO() g = _BytesGenerator(fp, mangle_from_=False) g.flatten(msgRoot, unixfrom=False) body = fp.getvalue() multipart_boundary = msgRoot.get_boundary() headers["content-type"] = ( "multipart/related; " 'boundary="%s"') % multipart_boundary url = _add_query_parameter(url, "uploadType", "multipart") logger.info("URL being requested: %s %s" % (httpMethod, url)) return self._requestBuilder( self._http, model.response, url, method=httpMethod, body=body, headers=headers, methodId=methodId, resumable=resumable, )