PageRenderTime 38ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/hyperkitty/lib/utils.py

https://gitlab.com/msapiro/hyperkitty
Python | 196 lines | 144 code | 18 blank | 34 comment | 12 complexity | eb9b0c78a08aa613fe6a3adcdf7f8b24 MD5 | raw file
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Copyright (C) 2014-2022 by the Free Software Foundation, Inc.
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either version 2
  8. # of the License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU General Public License
  16. # along with this program; if not, write to the Free Software
  17. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
  18. # USA.
  19. #
  20. # Author: Aurelien Bompard <abompard@fedoraproject.org>
  21. import email.utils
  22. import logging
  23. import os
  24. import os.path
  25. import re
  26. from base64 import b32encode
  27. from contextlib import contextmanager
  28. from datetime import timedelta
  29. from email.parser import BytesHeaderParser, HeaderParser
  30. from email.policy import default
  31. from hashlib import sha1
  32. from tempfile import gettempdir
  33. from django.conf import settings
  34. from django.db import connection
  35. from django.utils import timezone
  36. import dateutil.parser
  37. import dateutil.tz
  38. from flufl.lock import Lock
  39. log = logging.getLogger(__name__)
  40. def get_message_id_hash(msg_id):
  41. """
  42. Returns the X-Message-ID-Hash header for the provided Message-ID header.
  43. See <http://wiki.list.org/display/DEV/Stable+URLs#StableURLs-Headers> for
  44. details. Example:
  45. """
  46. msg_id = email.utils.unquote(msg_id).encode('utf-8')
  47. return b32encode(sha1(msg_id).digest()).decode('utf-8')
  48. def get_message_id(message):
  49. msg_id = email.utils.unquote(re.sub(r'\s', '', message['Message-Id']))
  50. # Protect against extremely long Message-Ids (there is no limit in the
  51. # email spec), it's set to VARCHAR(255) in the database
  52. if len(msg_id) >= 255:
  53. msg_id = msg_id[:254]
  54. return msg_id
  55. IN_BRACKETS_RE = re.compile("[^<]*<([^>]+)>.*")
  56. def get_ref(message):
  57. """
  58. Returns the message-id of the reference email for a given message.
  59. """
  60. if ("References" not in message and
  61. "In-Reply-To" not in message):
  62. return None
  63. ref_id = message.get("In-Reply-To")
  64. # EmailMessage will always return instances of str
  65. assert ref_id is None or isinstance(ref_id, str)
  66. if ref_id is None or not ref_id.strip():
  67. ref_id = message.get("References")
  68. if ref_id is not None and ref_id.strip():
  69. # There can be multiple references, use the last one
  70. ref_id = ref_id.split()[-1].strip()
  71. if ref_id is not None:
  72. if "<" in ref_id or ">" in ref_id:
  73. ref_id = IN_BRACKETS_RE.match(ref_id)
  74. if ref_id:
  75. ref_id = ref_id.group(1)
  76. if ref_id is not None:
  77. ref_id = ref_id[:254]
  78. return ref_id
  79. def parseaddr(address):
  80. """
  81. Wrapper around email.utils.parseaddr to also handle Mailman's generated
  82. mbox archives.
  83. """
  84. if address is None:
  85. return "", ""
  86. from_name, from_email = email.utils.parseaddr(address)
  87. if '@' not in from_email:
  88. address = address.replace(" at ", "@")
  89. from_name, from_email = email.utils.parseaddr(address)
  90. if not from_name:
  91. from_name = from_email
  92. return from_name, from_email
  93. def parsedate(datestring):
  94. if datestring is None:
  95. return None
  96. try:
  97. parsed = dateutil.parser.parse(datestring)
  98. except ValueError:
  99. return None
  100. try:
  101. offset = parsed.utcoffset()
  102. except ValueError:
  103. # Wrong offset, reset to UTC
  104. offset = None
  105. parsed = parsed.replace(tzinfo=timezone.utc)
  106. if offset is not None and \
  107. abs(offset) > timedelta(hours=13):
  108. parsed = parsed.astimezone(timezone.utc)
  109. if parsed.tzinfo is None:
  110. parsed = parsed.replace(tzinfo=timezone.utc) # make it aware
  111. return parsed
  112. def header_to_unicode(header):
  113. if header is None:
  114. header = str(header)
  115. if isinstance(header, str):
  116. msg = HeaderParser(policy=default).parsestr('dummy: ' + header)
  117. elif isinstance(header, bytes):
  118. msg = BytesHeaderParser(policy=default).parsebytes(b'dummy: ' + header)
  119. else:
  120. raise ValueError('header must be str or bytes, but is ' + type(header))
  121. return msg['dummy']
  122. def stripped_subject(mlist, subject):
  123. if mlist is None:
  124. return subject
  125. if not subject:
  126. return "(no subject)"
  127. if not mlist.subject_prefix:
  128. return subject
  129. if subject.lower().startswith(mlist.subject_prefix.lower()):
  130. subject = subject[len(mlist.subject_prefix):]
  131. return subject
  132. # File-based locking
  133. def run_with_lock(fn, *args, **kwargs):
  134. if kwargs.get('remove'):
  135. # remove = True is slow. We need to extend the lock life
  136. lock_life = getattr(settings,
  137. "HYPERKITTY_JOBS_UPDATE_INDEX_LOCK_LIFE", 900)
  138. else:
  139. # Use the default (15 sec)
  140. lock_life = None
  141. lock = Lock(getattr(
  142. settings, "HYPERKITTY_JOBS_UPDATE_INDEX_LOCKFILE",
  143. os.path.join(gettempdir(), "hyperkitty-jobs-update-index.lock")),
  144. lifetime=lock_life)
  145. if lock.is_locked:
  146. log.warning(
  147. "Update index lock is acquired by: {}".format(*lock.details))
  148. return
  149. with lock:
  150. try:
  151. fn(*args, **kwargs)
  152. except Exception as e:
  153. log.exception("Failed to update the fulltext index: %s", e)
  154. @contextmanager
  155. def pgsql_disable_indexscan():
  156. # Sometimes PostgreSQL chooses a very inefficient query plan:
  157. # https://pagure.io/fedora-infrastructure/issue/6164
  158. if connection.vendor != "postgresql":
  159. yield
  160. return
  161. with connection.cursor() as cursor:
  162. cursor.execute("SET enable_indexscan = OFF")
  163. try:
  164. yield
  165. finally:
  166. cursor.execute("SET enable_indexscan = ON")