mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 11:51:05 -05:00
Utils: add parse_rfc2822date
(Useful for interpreting date header in inbound messages)
This commit is contained in:
@@ -12,7 +12,7 @@ from django.conf import settings
|
|||||||
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
from django.core.mail.message import sanitize_address, DEFAULT_ATTACHMENT_MIME_TYPE
|
||||||
from django.utils.encoding import force_text
|
from django.utils.encoding import force_text
|
||||||
from django.utils.functional import Promise
|
from django.utils.functional import Promise
|
||||||
from django.utils.timezone import utc
|
from django.utils.timezone import utc, get_fixed_timezone
|
||||||
# noinspection PyUnresolvedReferences
|
# noinspection PyUnresolvedReferences
|
||||||
from six.moves.urllib.parse import urlsplit, urlunsplit
|
from six.moves.urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
@@ -466,3 +466,40 @@ def get_request_uri(request):
|
|||||||
url = urlunsplit((parts.scheme, basic_auth + '@' + parts.netloc,
|
url = urlunsplit((parts.scheme, basic_auth + '@' + parts.netloc,
|
||||||
parts.path, parts.query, parts.fragment))
|
parts.path, parts.query, parts.fragment))
|
||||||
return url
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from email.utils import parsedate_to_datetime # Python 3.3+
|
||||||
|
except ImportError:
|
||||||
|
from email.utils import parsedate_tz
|
||||||
|
|
||||||
|
# Backport Python 3.3+ email.utils.parsedate_to_datetime
|
||||||
|
def parsedate_to_datetime(s):
|
||||||
|
# *dtuple, tz = _parsedate_tz(data)
|
||||||
|
dtuple = parsedate_tz(s)
|
||||||
|
tz = dtuple[-1]
|
||||||
|
# if tz is None: # parsedate_tz returns 0 for "-0000"
|
||||||
|
if tz is None or (tz == 0 and "-0000" in s):
|
||||||
|
# "... indicates that the date-time contains no information
|
||||||
|
# about the local time zone" (RFC 2822 #3.3)
|
||||||
|
return datetime(*dtuple[:6])
|
||||||
|
else:
|
||||||
|
# tzinfo = datetime.timezone(datetime.timedelta(seconds=tz)) # Python 3.2+ only
|
||||||
|
tzinfo = get_fixed_timezone(tz // 60) # don't use timedelta (avoid Django bug #28739)
|
||||||
|
return datetime(*dtuple[:6], tzinfo=tzinfo)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rfc2822date(s):
|
||||||
|
"""Parses an RFC-2822 formatted date string into a datetime.datetime
|
||||||
|
|
||||||
|
Returns None if string isn't parseable. Returned datetime will be naive
|
||||||
|
if string doesn't include known timezone offset; aware if it does.
|
||||||
|
|
||||||
|
(Same as Python 3 email.utils.parsedate_to_datetime, with improved
|
||||||
|
handling for unparseable date strings.)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return parsedate_to_datetime(s)
|
||||||
|
except (IndexError, TypeError, ValueError):
|
||||||
|
# despite the docs, parsedate_to_datetime often dies on unparseable input
|
||||||
|
return None
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ except ImportError:
|
|||||||
string_concat = None
|
string_concat = None
|
||||||
|
|
||||||
from anymail.exceptions import AnymailInvalidAddress
|
from anymail.exceptions import AnymailInvalidAddress
|
||||||
from anymail.utils import (parse_address_list, ParsedEmail,
|
from anymail.utils import (
|
||||||
|
parse_address_list, ParsedEmail,
|
||||||
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
is_lazy, force_non_lazy, force_non_lazy_dict, force_non_lazy_list,
|
||||||
update_deep,
|
update_deep,
|
||||||
get_request_uri, get_request_basic_auth)
|
get_request_uri, get_request_basic_auth, parse_rfc2822date)
|
||||||
|
|
||||||
|
|
||||||
class ParseAddressListTests(SimpleTestCase):
|
class ParseAddressListTests(SimpleTestCase):
|
||||||
@@ -296,3 +297,35 @@ class RequestUtilsTests(SimpleTestCase):
|
|||||||
HTTP_AUTHORIZATION=self.basic_auth('user', 'pass'))
|
HTTP_AUTHORIZATION=self.basic_auth('user', 'pass'))
|
||||||
self.assertEqual(get_request_uri(request),
|
self.assertEqual(get_request_uri(request),
|
||||||
"https://user:pass@secret.example.com:8989/path/to/?query")
|
"https://user:pass@secret.example.com:8989/path/to/?query")
|
||||||
|
|
||||||
|
|
||||||
|
class ParseRFC2822DateTests(SimpleTestCase):
|
||||||
|
def test_with_timezones(self):
|
||||||
|
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 -0700")
|
||||||
|
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35-07:00")
|
||||||
|
self.assertIsNotNone(dt.utcoffset()) # aware
|
||||||
|
|
||||||
|
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 +0700")
|
||||||
|
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35+07:00")
|
||||||
|
self.assertIsNotNone(dt.utcoffset()) # aware
|
||||||
|
|
||||||
|
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 +0000")
|
||||||
|
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35+00:00")
|
||||||
|
self.assertIsNotNone(dt.tzinfo) # aware
|
||||||
|
|
||||||
|
def test_without_timezones(self):
|
||||||
|
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35 -0000") # "no timezone information"
|
||||||
|
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35")
|
||||||
|
self.assertIsNone(dt.tzinfo) # naive (compare with +0000 version in previous test)
|
||||||
|
|
||||||
|
dt = parse_rfc2822date("Tue, 24 Oct 2017 10:11:35")
|
||||||
|
self.assertEqual(dt.isoformat(), "2017-10-24T10:11:35")
|
||||||
|
self.assertIsNone(dt.tzinfo) # naive
|
||||||
|
|
||||||
|
def test_unparseable_dates(self):
|
||||||
|
self.assertIsNone(parse_rfc2822date(""))
|
||||||
|
self.assertIsNone(parse_rfc2822date(" "))
|
||||||
|
self.assertIsNone(parse_rfc2822date("garbage"))
|
||||||
|
self.assertIsNone(parse_rfc2822date("Tue, 24 Oct"))
|
||||||
|
self.assertIsNone(parse_rfc2822date("Lug, 24 Nod 2017 10:11:35 +0000"))
|
||||||
|
self.assertIsNone(parse_rfc2822date("Tue, 99 Oct 9999 99:99:99 +9999"))
|
||||||
|
|||||||
Reference in New Issue
Block a user