This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
python-emails/emails/utils.py

219 lines
6.9 KiB
Python

# encoding: utf-8
from __future__ import unicode_literals
import emails
import requests
from emails.exc import HTTPLoaderError
__all__ = ['parse_name_and_email', 'load_email_charsets', 'MessageID']
import socket
import time
import os
import random
import email.charset
from email import generator
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import Header, decode_header
from email.utils import formataddr, parseaddr
from emails.compat import string_types, to_unicode, NativeStringIO, is_py2, BytesIO
_charsets_loaded = False
CHARSETS_FIX = [
['windows-1251', 'QP', 'QP'],
# koi8 should by send as Quoted Printable because of bad SpamAssassin reaction on base64 (2008)
['koi8-r', 'QP', 'QP'],
['utf-8', 'BASE64', 'BASE64']
]
def load_email_charsets():
global _charsets_loaded
if not _charsets_loaded:
for (charset, header_enc, body_enc) in CHARSETS_FIX:
email.charset.add_charset(charset,
getattr(email.charset, header_enc),
getattr(email.charset, body_enc),
charset)
# Django's CachedDnsName:
# Cached the hostname, but do it lazily: socket.getfqdn() can take a couple of
# seconds, which slows down the restart of the server.
class CachedDnsName(object):
def __str__(self):
return self.get_fqdn()
def get_fqdn(self):
if not hasattr(self, '_fqdn'):
self._fqdn = socket.getfqdn()
return self._fqdn
DNS_NAME = CachedDnsName()
def getheader(header_text, default="ascii"):
"""Decode the specified header"""
headers = decode_header(header_text)
header_sections = [unicode(text, charset or default)
for text, charset in headers]
return u"".join(header_sections)
class MessageID:
"""Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
<20020201195627.33539.96671@nightshade.la.mastaler.com>
Optional idstring if given is a string used to strengthen the
uniqueness of the message id.
Based on django.core.mail.message.make_msgid
"""
def __init__(self, domain=None, idstring=None):
self.domain = domain or DNS_NAME
self.idstring = idstring
def __call__(self):
timeval = time.time()
utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
try:
pid = os.getpid()
except AttributeError:
# No getpid() in Jython, for example.
pid = 1
randint = random.randrange(100000)
if self.idstring is None:
idstring = ''
else:
idstring = '.' + self.idstring
msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, self.domain)
return msgid
def parse_name_and_email(obj, encoding='utf-8'):
# In: 'john@smith.me' or '"John Smith" <john@smith.me>' or ('John Smith', 'john@smith.me')
# Out: (u'John Smith', u'john@smith.me')
_realname = ''
_email = ''
if isinstance(obj, (list, tuple)):
if len(obj) == 2:
_realname, _email = obj
elif isinstance(obj, string_types):
_realname, _email = parseaddr(obj)
else:
raise ValueError("Can not parse_name_and_email from %s" % obj)
if isinstance(_realname, bytes):
_realname = to_unicode(_realname, encoding)
if isinstance(_email, bytes):
_email = to_unicode(_email, encoding)
return _realname or None, _email or None
def sanitize_address(addr, encoding):
if isinstance(addr, string_types):
addr = parseaddr(to_unicode(addr))
nm, addr = addr
# This try-except clause is needed on Python 3 < 3.2.4
# http://bugs.python.org/issue14291
try:
nm = Header(nm, encoding).encode()
except UnicodeEncodeError:
nm = Header(nm, 'utf-8').encode()
try:
addr.encode('ascii')
except UnicodeEncodeError: # IDN
if '@' in addr:
localpart, domain = addr.split('@', 1)
localpart = str(Header(localpart, encoding))
domain = domain.encode('idna').decode('ascii')
addr = '@'.join([localpart, domain])
else:
addr = Header(addr, encoding).encode()
return formataddr((nm, addr))
class MIMEMixin():
def as_string(self, unixfrom=False, linesep='\n'):
"""Return the entire formatted message as a string.
Optional `unixfrom' when True, means include the Unix From_ envelope
header.
This overrides the default as_string() implementation to not mangle
lines that begin with 'From '. See bug #13433 for details.
"""
fp = NativeStringIO()
g = generator.Generator(fp, mangle_from_=False)
if is_py2:
g.flatten(self, unixfrom=unixfrom)
else:
g.flatten(self, unixfrom=unixfrom, linesep=linesep)
return fp.getvalue()
if is_py2:
as_bytes = as_string
else:
def as_bytes(self, unixfrom=False, linesep='\n'):
"""Return the entire formatted message as bytes.
Optional `unixfrom' when True, means include the Unix From_ envelope
header.
This overrides the default as_bytes() implementation to not mangle
lines that begin with 'From '. See bug #13433 for details.
"""
fp = BytesIO()
g = generator.BytesGenerator(fp, mangle_from_=False)
g.flatten(self, unixfrom=unixfrom, linesep=linesep)
return fp.getvalue()
class SafeMIMEText(MIMEMixin, MIMEText):
def __init__(self, text, subtype, charset):
self.encoding = charset
MIMEText.__init__(self, text, subtype, charset)
def __setitem__(self, name, val):
MIMEText.__setitem__(self, name, val)
class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params):
self.encoding = encoding
MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
def __setitem__(self, name, val):
MIMEMultipart.__setitem__(self, name, val)
DEFAULT_REQUESTS_PARAMS = dict(allow_redirects=True,
verify=False, timeout=10,
headers={'User-Agent': emails.USER_AGENT})
def fetch_url(url, valid_http_codes=(200, ), requests_args=None):
args = {}
args.update(DEFAULT_REQUESTS_PARAMS)
args.update(requests_args or {})
r = requests.get(url, **args)
if valid_http_codes and (r.status_code not in valid_http_codes):
raise HTTPLoaderError('Error loading url: %s. HTTP status: %s' % (url, r.status_code))
return r
def encode_header(value, charset='utf-8'):
value = to_unicode(value, charset=charset)
if isinstance(value, string_types):
value = value.rstrip()
_r = Header(value, charset)
return str(_r)
else:
return value